Configuration
bazel-mcp works without a configuration file. The built-in defaults allow
any Bazel workspace available to the current user and retain invocation
evidence in a local cache.
For shared environments or tighter local controls, configure allowed workspace roots, command policy, retention, timeouts, environment variables, and redaction explicitly.
Loading configuration
Section titled “Loading configuration”The server looks for configuration in this order:
- the file passed with
--config; - the file named by
BAZEL_MCP_CONFIG; $XDG_CONFIG_HOME/bazel-mcp/config.toml, or~/.config/bazel-mcp/config.tomlwhenXDG_CONFIG_HOMEis not set;- built-in defaults.
The default path is used only when the file already exists. Command-line
--allow-root values are added to roots read from the file, and --cache-root
overrides the configured cache directory.
Unknown top-level settings and unknown keys in the [aspect] and [starlark]
tables are rejected so misspelled controls cannot silently fall back to
defaults.
Start with the repository’s example configuration:
mkdir -p ~/.config/bazel-mcpcp examples/config.toml ~/.config/bazel-mcp/config.tomlReplace the example workspace path before starting the server.
Common settings
Section titled “Common settings”Restrict workspace access
Section titled “Restrict workspace access”An empty allowed_roots list permits every workspace available to the current
user. Production or shared configurations should list one or more absolute
roots:
allowed_roots = ["/work/company", "/work/open-source"]A requested workspace must be contained by one of these roots. The invocation cache cannot be located inside an allowed workspace root.
For a single root, the equivalent CLI option is:
bazel-mcp --allow-root /work/companySelect Bazel
Section titled “Select Bazel”By default, bazel-mcp looks for an executable in this order:
tools/bazelin the requested workspace;bazeliskonPATH;bazelonPATH.
Set an explicit executable to bypass discovery:
bazel_executable = "/usr/local/bin/bazelisk"Route commands through Aspect CLI
Section titled “Route commands through Aspect CLI”Aspect support is disabled by default. To run aspect lint for a lint
request, add the command to both the normal command allowlist and the Aspect
route list:
allowed_commands = [ "aquery", "build", "coverage", "cquery", "help", "info", "lint", "mod", "query", "test", "version",]
[aspect]commands = ["lint"]# executable = "/opt/homebrew/bin/aspect"allow_workspace_mutation = falseWhen aspect.executable is omitted, the server resolves aspect from its
PATH. The underlying Bazel executable still follows the normal discovery
rules and is passed to Aspect through BAZEL_REAL; commands not listed in
aspect.commands continue to invoke Bazel directly.
The run command cannot be routed through Aspect CLI; its privacy controls
require the direct Bazel driver.
For build-like Aspect commands (build, test, and lint), bazel-mcp starts
its loopback BES even when bep_transport = "tail". Aspect keeps ownership of
its internal BEP stream and forwards a copy to the private local capture. Any
additional Aspect --bes-backend argument remains available for a remote sink.
Startup arguments use the ordinary startup_args request field; callers cannot
inject Aspect’s --bazel-startup-flag, task identity, or local capture header.
aspect lint --fix is rejected unless allow_workspace_mutation = true.
Configured Aspect tasks are operator-trusted code and may have other side
effects that bazel-mcp cannot infer from their names.
An explicit executable is especially useful when bazel-mcp is installed as
a symlink named bazel for agent-mode CLI use. Executable discovery skips the
currently running shim and fails instead of recursively launching it.
Pass environment variables
Section titled “Pass environment variables”Child Bazel processes always receive HOME, PATH, TMPDIR, TEMP, TMP,
and USER when those variables are present. Add other variable names to the
allowlist when they are required by credentials, toolchains, or remote
execution:
environment_allowlist = ["GOOGLE_APPLICATION_CREDENTIALS", "JAVA_HOME"]Redact sensitive text
Section titled “Redact sensitive text”Configured regular expressions are replaced with [REDACTED] before text is
written to summaries, durable metadata, or telemetry:
redaction_patterns = [ "(?i)authorization: bearer [^\\s]+", "(?i)token=[^\\s]+",]Raw evidence remains local and should still be treated as sensitive.
Enable Bazel run
Section titled “Enable Bazel run”The Bazel run command is denied by default because it executes a built target.
To opt in, add run to allowed_commands and remove it from
denied_commands. Configuration arrays replace the defaults, so retain every
other command your clients need:
allowed_commands = [ "aquery", "build", "coverage", "cquery", "help", "info", "mod", "query", "run", "test", "version",]denied_commands = ["clean", "fetch", "mobile-install", "shutdown", "sync"]A run request uses separate Bazel and program argument fields:
{ "workspace": "/src/project", "command": "run", "args": ["--config=dev"], "target": "//cmd/example", "program_args": ["--format=json", "input.txt"], "timeout_seconds": 300}target is required and singular. args may contain Bazel flags only; valued
options use --flag=value. program_args is passed after a server-owned --
delimiter and is always replaced by placeholders in stored metadata, canonical
arguments, reducer input, and telemetry projections. The server also reserves
run options that could bypass this boundary, including --script_path,
--run_under, --run_env, subcommand logging, and the BEP residue controls.
Run support is Unix-only and non-interactive: stdin is connected to null, no
PTY is allocated, and the invocation keeps Bazel’s native output-base lock for
the lifetime of the foreground program. Use it for finite commands rather than
long-lived servers. The combined raw stdout and stderr ceiling defaults to 256
MiB and is configured with maximum_run_output_bytes.
Choose BEP transport
Section titled “Choose BEP transport”bep_transport = "tail"tail is the backwards-compatible default. Bazel writes the private binary
BEP file directly, so an existing remote --bes_backend or BuildBuddy setup
continues to receive events.
fifo is an opt-in POSIX optimization. bazel-mcp creates a private named pipe,
feeds its ordered frames through the shared capture pipeline, and commits the
exact bytes to events.bep before the reduction subscriber observes them. It
probes the persistent Bazel server PID
and separately tracks the spawned invocation-client PID so a writer may close
and reconnect during a Bazel retry without EOF ending the capture. FIFO setup
or PID-discovery failures fall back to tail; Windows always uses that portable
fallback.
bes starts a plaintext gRPC Build Event Service on an ephemeral loopback
port and configures Bazel to publish to it with
--bes_upload_mode=wait_for_upload_complete. The service validates the
invocation ID and stream sequence with Buffa views, then reconstructs the same
private varint-delimited events.bep file through the shared capture pipeline.
Small bounded event batches amortize transport handoff, but Bazel receives no
acknowledgement until the corresponding raw frames have been accepted by the
private evidence writer. Reduction observes those frames only afterward.
Select this mode explicitly because Bazel supports only one --bes_backend;
caller-supplied remote BES flags are rejected in this mode. The listener is
never exposed outside the local host.
See BEP transport performance for benchmark methodology, current results, and reproduction commands.
Choose a result encoding
Section titled “Choose a result encoding”result_encoding = "toon"Available values are:
| Value | MCP result |
|---|---|
text |
Compact JSON in one text content block. |
toon |
Token-oriented TOON text in one content block. This is the default. |
structured |
MCP structured content only. |
both |
Structured content plus backwards-compatible JSON text. |
Configure negotiated task execution
Section titled “Configure negotiated task execution”mcp_execution_policy = "auto"task_ttl_seconds = 86400task_poll_interval_ms = 2000auto is the recommended default. The server discovers support at runtime and
uses synchronous execution for ordinary clients, the experimental task flow
for clients negotiating MCP 2025-11-25 and sending params.task, or the
io.modelcontextprotocol/tasks extension for clients negotiating its
2026-06-30 base protocol and declaring the extension in per-request
capabilities. The task dialect is never selected from a host name.
sync_only always returns an ordinary CallToolResult for new calls. Existing
unexpired task handles remain readable and cancellable after a restart or
policy change. tasks_required rejects bazel.run before starting Bazel when
the client did not declare a compatible task flow.
The TTL is the minimum time a terminal task result remains available. A task that is still queued or running never expires. The poll interval is advisory and must be between 100 and 60,000 milliseconds.
Load custom reducers
Section titled “Load custom reducers”Built-in Rust reducers remain active when custom reducers are configured. Starlark files must be listed explicitly:
[starlark]files = ["reducers/custom_compiler.star"]Relative paths are resolved against the directory containing the configuration file and canonicalized at startup. Missing, duplicate, invalid, or incompatible reducers prevent startup. Bazel workspaces are never searched for reducer files, so merely checking out a repository cannot execute its code in the server.
All Starlark limits are operator-configurable under [starlark], although the
defaults are intended for ordinary diagnostic reducers. Runtime failures keep
the native result and add a bounded note. See the
custom reducer guide for the API and security model.
Reference
Section titled “Reference”| Setting | Default | Description |
|---|---|---|
allowed_roots |
[] |
Absolute roots containing workspaces the server may access. An empty list allows any workspace. |
cache_root |
Platform user cache under bazel-mcp |
Shared directory for metadata, logs, and BEP evidence. Multiple server processes may use the same root concurrently. |
bep_transport |
tail |
BEP ingestion path: portable private binary file (tail), opt-in POSIX named pipe with file fallback (fifo), or loopback Build Event Service (bes). |
bazel_executable |
unset | Explicit Bazel or Bazelisk executable. |
aspect.commands |
[] |
Commands routed through Aspect CLI. Each must also be present in allowed_commands; run is not supported. |
aspect.executable |
unset | Explicit Aspect CLI executable. When unset and Aspect routing is enabled, resolves aspect from PATH. |
aspect.allow_workspace_mutation |
false |
Permit known mutation arguments such as aspect lint --fix; configured tasks themselves remain operator-trusted. |
output_user_root |
unset | Isolated Bazel output user root managed by the server. |
allowed_commands |
build, test, coverage, query commands, and selected informational commands | Commands eligible to run. Add run explicitly to opt in. |
denied_commands |
clean, fetch, mobile-install, run, shutdown, sync |
Commands rejected even if also present in allowed_commands. |
environment_allowlist |
[] |
Additional environment variables passed to Bazel. |
redaction_patterns |
[] |
Regular expressions removed from model-visible and persisted text fields. |
global_concurrency |
4 |
Maximum concurrent Bazel invocations. |
maximum_pending_invocations |
256 |
Maximum queued and running invocations. Must be at least global_concurrency. |
default_timeout_seconds |
1800 |
Timeout used when a request omits one. |
maximum_timeout_seconds |
7200 |
Maximum timeout accepted from a request. |
maximum_run_output_bytes |
268435456 |
Combined raw stdout and stderr ceiling for a Bazel run invocation. |
cancellation_interrupt_grace_seconds |
10 |
Time allowed after the initial interrupt. |
cancellation_terminate_grace_seconds |
5 |
Additional time allowed after termination. |
progress_initial_seconds |
30 |
Delay before the first MCP progress notification. |
progress_interval_seconds |
60 |
Interval between later progress notifications. |
mcp_execution_policy |
auto |
New-run policy: auto, sync_only, or tasks_required. |
task_ttl_seconds |
86400 |
Minimum terminal task-result availability window; must be greater than zero. |
task_poll_interval_ms |
2000 |
Suggested task polling interval, from 100 through 60,000 ms. |
retention_days |
7 |
Maximum age of retained invocation evidence. |
maximum_storage_bytes |
10737418240 |
Maximum cache size before older evidence is removed. |
retention_cleanup_interval_seconds |
3600 |
Interval between retention sweeps. |
result_encoding |
toon |
Model-visible result representation. |
supported_bazel_major_versions |
[8, 9] |
Bazel major versions accepted by default. |
allow_unsupported_bazel_versions |
false |
Allow majors outside supported_bazel_major_versions. |
version_check_timeout_seconds |
30 |
Timeout for the pre-invocation Bazel version check. |
isolated_bazel_server_idle_seconds |
60 |
Bazel server idle timeout used with output_user_root. |
starlark.files |
[] |
Explicit custom reducer files. Relative paths are resolved from the configuration file. |
starlark.max_source_bytes |
262144 |
Maximum UTF-8 source size per reducer. |
starlark.max_input_bytes |
1048576 |
Maximum normalized stdout/stderr input and the basis for bounded baseline data. |
starlark.max_events |
10000 |
Maximum normalized BEP events retained for custom reducers. |
starlark.max_output_bytes |
65536 |
Maximum serialized patch size per reducer. |
starlark.max_output_items |
1000 |
Maximum diagnostics returned per reducer. |
starlark.max_ticks |
1000000 |
Starlark evaluator instruction budget. |
starlark.max_heap_bytes |
16777216 |
Starlark evaluator heap budget. |
starlark.max_callstack_size |
100 |
Maximum Starlark call-stack depth. |
starlark.timeout_ms |
100 |
Best-effort wall-clock evaluation limit per reducer. |
Shared output bases
Section titled “Shared output bases”Every Bazel request participates in user-scoped, cross-process coordination
using the known effective output-base key. An explicit --output_base is keyed
by its canonical path; otherwise the canonical workspace is the conservative
key. The lock is request-scoped, so separate bazel-mcp processes that share an
explicit output base wait before spawning their Bazel clients.
This coordination does not change Bazel startup arguments and does not reject a
shared output base across workspaces. Direct Bazel clients, editor integrations,
and output-base choices hidden in bazelrc still use Bazel’s native blocking lock
and normal server takeover/restart behavior. While either wait is active,
synchronous MCP progress reports phase=output_base_lock_wait; completed
invocations expose the combined duration as output_base_lock_wait_ms in the
metrics view. Owner labels are deliberately bounded and omit workspace paths.
CLI options
Section titled “CLI options”Run bazel-mcp --help for the MCP server option reference:
| Option | Description |
|---|---|
--config <PATH> |
Read configuration from a TOML file. |
--allow-root <PATH> |
Add an allowed workspace root. May be repeated. |
--cache-root <PATH> |
Override the invocation cache directory. |
--log <FILTER> |
Set the tracing filter written to stderr. |
Standard output is reserved for MCP protocol frames. Tracing and diagnostics are always written to standard error.
Agent-mode CLI
Section titled “Agent-mode CLI”Agent mode exposes the bounded bazel.run presentation to harnesses that can
invoke only a Bazel-shaped CLI. Any of these forms select it:
bazel-mcp passthrough -- build //app:serverBAZEL_MCP_MODE=agent bazel-mcp test //services/...bazel query 'deps(//app:server)'The third form applies when the launched executable filename is bazel, such
as a symlink to bazel-mcp. BAZEL_MCP_CONFIG and the normal default config
path select configuration; Bazel-shaped argv is not parsed for MCP-only
--config, --allow-root, or --cache-root options.
The CLI discovers the nearest workspace ancestor, applies the configured
command and argument policy, captures through the ordinary runner, prints the
configured bounded result, and preserves Bazel’s exit code. Capture uses a
private temporary filesystem store that is deleted before exit. Consequently
available_views is empty, inspect_hint is omitted, and a truncated result
or other normally inspectable detail produces a rerun_hint instead of
promising retained evidence.
Put --no-agent-mode first to bypass capture and filtering:
bazel --no-agent-mode query --output=label 'deps(//app:server)'The bypass invokes the resolved Bazel directly with inherited stdio and the
original remaining arguments. It intentionally bypasses agent-mode command and
flag policy. BAZEL_MCP_LOG optionally enables wrapper tracing in agent mode;
the default emits warnings and errors only.