Run Bazel from MCP-compatible coding agents without filling their context windows with build logs.
bazel-mcp is a local Model Context Protocol
server. It runs Bazel in your workspace, returns a compact actionable result,
and keeps the complete invocation evidence available for inspection when an
agent needs more detail.
Read the documentation for installation, guided workflows, configuration, architecture, and reproducible benchmarks.
Bazel output can be enormous. Sending complete progress output, repeated warnings, and test logs to a coding agent wastes context and can hide the error that matters.
bazel-mcp gives agents:
- concise summaries of successful and failed invocations;
- structured diagnostics, test results, coverage, artifacts, and query output;
- filtered, paginated access to retained evidence;
- cancellation of queued or running commands.
Bazel still runs locally with your workspace, configured credentials, toolchains, and remote execution settings.
- macOS, Linux, or Windows x86_64 (preview)
- Bazel 8 or 9, Bazelisk, or an executable workspace-local
tools/bazel - an MCP-compatible client
- Bazelisk when building from source (the repository pins Bazel and Rust)
brew install ewhauser/tap/bazel-mcpDownload a prebuilt archive from the latest GitHub release, or run the shell installer on macOS or Linux:
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/ewhauser/bazel-mcp/releases/latest/download/bazel-mcp-server-installer.sh | shOn Windows, run the PowerShell installer:
irm https://github.com/ewhauser/bazel-mcp/releases/latest/download/bazel-mcp-server-installer.ps1 | iexClone the repository and build the server with the pinned Bazel and Rust toolchains:
git clone https://github.com/ewhauser/bazel-mcp.git
cd bazel-mcp
bazelisk build -c opt //:bazel-mcpThe binary is written to bazel-bin/crates/bazel-mcp-server/bazel-mcp.
Register the binary as a stdio MCP server. The exact settings location depends on your client.
{
"mcpServers": {
"bazel": {
"command": "bazel-mcp"
}
}
}If you built from source without installing the binary, use the absolute path
to bazel-bin/crates/bazel-mcp-server/bazel-mcp instead.
Restart the client, open a Bazel workspace, and try prompts such as:
- “Build
//app:server.” - “Run the tests under
//services/...and explain any failures.” - “Which targets depend on
//lib:core?”
No configuration file is required. By default, the server can run Bazel in any workspace accessible to the current user. See Security and local data to restrict it to specific roots.
Harnesses that can launch only a Bazel-style command can use the same bounded result without speaking MCP:
bazel-mcp passthrough -- test //services/...Agent mode is also selected when BAZEL_MCP_MODE=agent is set or when the
executable filename is bazel, for example through a symlink placed earlier on
the harness's PATH. It accepts ordinary Bazel startup arguments, a configured
command, and command arguments. The current workspace is discovered by walking
upward from the launch directory.
Agent mode captures and reduces output through the same runner as bazel.run,
prints the configured JSON or TOON representation, and exits with Bazel's exit
code. Its raw output and BEP files live only in an invocation-scoped temporary
directory that is removed before the command returns. Because there is no
retained invocation, the result does not advertise bazel.inspect; truncated
or otherwise omitted detail instead produces a hint for an unfiltered rerun:
bazel --no-agent-mode test //services/...--no-agent-mode must be the first argument. It executes the resolved real
Bazel with inherited standard input, output, and error streams. Configure
bazel_executable when the shim would otherwise be the only bazel on PATH;
the wrapper refuses to resolve itself recursively.
The server exposes three tools:
| Tool | Purpose |
|---|---|
bazel.run |
Run an allowed Bazel or configured Aspect command and return a bounded summary. |
bazel.inspect |
Read filtered diagnostics, tests, coverage, artifacts, query results, or logs from an invocation. |
bazel.cancel |
Cancel a queued or running invocation. |
bazel.run supports build, test, coverage, query, cquery, aquery,
selected informational commands, and an opt-in form of the Bazel run
command. Operators may also route explicitly allowed commands such as lint
through Aspect CLI. Successful results are limited to 2 KiB and unsuccessful
results to 8 KiB. Follow-up bazel.inspect calls are also bounded and
paginated, so an agent only retrieves the evidence it needs.
The Bazel run command is denied by default. Once enabled by server policy, it
accepts one explicit target and a separate sensitive program_args list:
{
"workspace": "/src/project",
"command": "run",
"args": ["--config=dev"],
"target": "//cmd/example",
"program_args": ["--format=json", "input.txt"],
"timeout_seconds": 300
}The server owns the -- boundary, omits run residue from BEP projections,
stores program-argument placeholders instead of values, and connects stdin to
null. This mode is intended for finite, non-interactive Unix programs. It does
not provide a PTY or background-service lifecycle. Bazel run always uses the
direct Bazel driver rather than Aspect CLI. See
configuration for the opt-in policy.
Failure results rank concrete root causes before aggregated action failures.
Equivalent fanout failures are represented once with target: null and a
repetition_count. Test and coverage commands use Bazel's
--test_output=errors. Failed-test logs are copied into private invocation
storage before they are exposed; test results report test_log_available or an
explicit test_log_unavailable_reason, never a synthetic or local failure-log
URI.
The summary view returns structured counts and bounded diagnostics. The log
and test_log views deliberately have the simpler logical shape below in every
result encoding:
{
"invocation_id": "019...",
"view": "test_log",
"items": [
"[//foo:foo_test] assertion error: expected 3, received 4"
],
"next_cursor": null,
"truncated": false
}The MCP shape has no stdout/stderr field or selector. The server automatically normalizes, redacts, exactly deduplicates, filters, and sequences both captured streams. Requested item limits are maxima; serialized byte packing may return fewer items with an opaque cursor that resumes after the last emitted item.
Long calls can be returned as durable task handles when the MCP client declares
task support. With the default auto policy, the server discovers the
negotiated protocol and chooses synchronous execution, MCP 2025-11-25 legacy
tasks, or the io.modelcontextprotocol/tasks extension at runtime. This does
not add tools: task status, result, and cancellation are protocol methods.
MCP clients normally produce these messages on an agent's behalf, but the wire
shape is useful when integrating or debugging a host. bazel-mcp uses
newline-delimited JSON-RPC 2.0 over stdio; the examples below are formatted
across lines for readability and omit unrelated response fields.
| Flow | Client signal | Initial result | Final result |
|---|---|---|---|
| Synchronous | No task capability, or sync_only policy |
CallToolResult |
Same tools/call response |
| Legacy tasks | MCP 2025-11-25 plus params.task |
Nested result.task |
Separate tasks/result call |
| Tasks extension | Extension capability in request _meta |
Flat resultType: "task" |
Inline in terminal tasks/get |
Show representative JSON-RPC messages
A basic call is an ordinary MCP tool request:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "bazel.run",
"arguments": {
"workspace": "/src/project",
"command": "build",
"args": ["//app:server"]
}
}
}Without a compatible task opt-in, the request remains attached and returns a
normal CallToolResult. The default toon encoding places the bounded logical
result in one text content block:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [{
"type": "text",
"text": "invocation_id: \"019f...\"\nstate: succeeded\ncommand: build\nexit_code: 0\nheadline: Build succeeded\nmore_available: true"
}],
"isError": false
}
}That invocation_id can be passed to bazel.inspect or bazel.cancel.
Different result encodings change the content representation, not the logical
result or its byte budget.
A client negotiating MCP 2025-11-25 sees
bazel.run.execution.taskSupport = "optional". It opts into detached execution
by adding params.task to the same tool call:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "bazel.run",
"arguments": {
"workspace": "/src/project",
"command": "test",
"args": ["//services/..."]
},
"task": {}
}
}The response contains a nested task. Its taskId is also the Bazel invocation
ID, and it is readable as soon as the handle is returned:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"task": {
"taskId": "019f...",
"status": "working",
"ttl": 86400000,
"pollInterval": 2000
},
"_meta": {
"io.modelcontextprotocol/related-task": {
"taskId": "019f..."
}
}
}
}The client polls tasks/get, waits for the original CallToolResult with
tasks/result, lists durable handles with tasks/list, or requests
tasks/cancel.
Modern extension-aware clients can discover the capability with
server/discover:
{
"jsonrpc": "2.0",
"id": 4,
"method": "server/discover",
"params": {}
}{
"jsonrpc": "2.0",
"id": 4,
"result": {
"capabilities": {
"extensions": {
"io.modelcontextprotocol/tasks": {}
}
}
}
}After negotiating the extension's 2026-06-30 base protocol, each participating
request declares the extension in _meta. The initial result is flat rather
than nested:
{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "bazel.run",
"arguments": {
"workspace": "/src/project",
"command": "build",
"args": ["//app:server"]
},
"_meta": {
"io.modelcontextprotocol/clientCapabilities": {
"extensions": {
"io.modelcontextprotocol/tasks": {}
}
}
}
}
}{
"jsonrpc": "2.0",
"id": 5,
"result": {
"resultType": "task",
"taskId": "019f...",
"status": "working",
"ttlMs": 86400000,
"pollIntervalMs": 2000
}
}Here the client polls tasks/get; once terminal, that response contains the
original CallToolResult inline in its result field. The extension has no
tasks/result or tasks/list methods. Both task dialects are durable across a
server restart, while clients that declare neither continue to use the simpler
synchronous shape.
See specification 003 for the complete negotiation matrix, cancellation semantics, error codes, and pinned protocol revisions.
bazel-mcp executes Bazel with the permissions of the user who started the MCP
client. It does not make untrusted repositories safe; use a sandbox or isolated
account for untrusted source.
The server:
- invokes Bazel directly without shell evaluation or concatenated arguments;
- denies
clean,fetch,mobile-install,run,shutdown, andsyncby default; - prevents requests from overriding server-owned BEP and output-root flags;
- filters the child-process environment and supports configurable regex redaction;
- stores raw output and BEP evidence only in the local cache, using private file permissions.
To restrict the server to one or more workspace roots, add a repeated
--allow-root argument to the MCP configuration:
{
"mcpServers": {
"bazel": {
"command": "bazel-mcp",
"args": ["--allow-root", "/absolute/path/to/workspaces"]
}
}
}See SECURITY.md for the security policy and threat-model guidance.
The built-in defaults cover personal local use. For workspace restrictions,
retention limits, timeouts, command policy, result encoding, BEP transport,
optional Aspect CLI routing, custom redaction, or custom reducers, start with
examples/config.toml.
BEP capture defaults to the private binary-file (tail) path so existing
remote BES and BuildBuddy configurations keep working. Set
bep_transport = "fifo" for the measured POSIX named-pipe optimization (with
automatic file-tail fallback on Windows or setup failure), or
bep_transport = "bes" to use bazel-mcp's loopback gRPC Build Event Service.
All three modes feed one ordered capture pipeline: raw frames are committed or
verified in the private evidence file before reduction observes them, and only
redacted projections may reach metadata, telemetry, or model-visible output.
See BEP transport performance for the
design tradeoffs, measured results, and reproduction commands.
Pass a configuration explicitly with --config, set BAZEL_MCP_CONFIG, or
place it at $XDG_CONFIG_HOME/bazel-mcp/config.toml (normally
~/.config/bazel-mcp/config.toml). Command-line options can also add allowed
roots or override the cache directory.
See the configuration reference for all settings and their defaults.
Built-in Rust reducers remain enabled by default. Operators can explicitly load Starlark files to add diagnostics for custom Bazel rules or replace matching built-in diagnostics while preserving the native invocation outcome and local evidence:
[starlark]
files = ["reducers/custom_compiler.star"]Reducer paths are relative to the configuration file. No reducer is discovered from a Bazel workspace automatically. See the custom reducer guide for the versioned API, selector and override semantics, resource limits, trust model, and example.
TOON is the default model-visible result format. Select a different format in the server configuration when required by an MCP host:
result_encoding = "toon"| Value | Representation | When to use it |
|---|---|---|
toon |
One token-oriented TOON text block. | Default; use for compact model context. |
text |
One compact JSON text block. | Use when a host or downstream tool expects JSON text. |
structured |
MCP structuredContent only. |
Use with hosts verified to consume structured content without duplicating it into model context. |
both |
Structured content plus compact JSON text. | Compatibility mode for hosts that need both representations; it has the largest model-visible footprint. |
The setting applies to the server, not individual tool calls. Restart the MCP server after changing it. All four choices carry the same logical result, redaction, and byte ceilings; they only change its model-visible representation.
| Component | Supported |
|---|---|
| Bazel | Major versions 8 and 9 |
| Platforms | macOS and Linux; Windows x86_64 preview |
| Transport | Local MCP over stdio |
| Bazel discovery | tools/bazel, then Bazelisk, then Bazel on PATH |
| Task execution | Synchronous fallback, MCP 2025-11-25 legacy tasks, and the SEP-2663 Tasks extension |
Other Bazel major versions are rejected before an invocation is created unless they are explicitly enabled in the configuration.
Use mcp_execution_policy = "sync_only" if a host cannot consume task-shaped
results. Use tasks_required only when detached execution is mandatory; calls
from clients without compatible capabilities are then rejected before Bazel
starts. See the configuration reference
for TTL and polling settings.
Across three independent five-sample runs against a pinned Abseil corpus,
bazel-mcp reduced cumulative model context by 89.73–90.69% compared with a
default shell agent. The latest run measured an 89.73% reduction, with 0.00%
median paired Bazel wall-time overhead.
These results are deterministic tokenizer estimates, not provider billing. See the benchmark methodology for the complete results, corpus, acceptance gates, and reproduction commands.
In a five-sample run over two high-output Abseil coding tasks,
gpt-5.6-luna at xhigh reasoning solved all 10 attempts with both the direct
shell and compact-JSON MCP adapters. The JSON MCP arm reduced
provider-reported total tokens by 36.88% and active tokens by 41.77%
when cached input was assigned zero weight.
| Metric | Shell Bazel | bazel-mcp |
Reduction |
|---|---|---|---|
| Verified solves | 10/10 | 10/10 | parity |
| Total tokens | 2,444,393 | 1,542,870 | 36.88% |
| Active tokens, 0% cached-input weight | 479,593 | 279,254 | 41.77% |
| Agent time | 653.2s | 637.1s | 2.46% |
A second five-sample run compared the same MCP tools using compact JSON and TOON. Both encodings again solved every attempt, while TOON reduced total provider tokens by 11.99% and the retained MCP result payload by 35.69%.
| Metric | Compact JSON | TOON | Reduction |
|---|---|---|---|
| Verified solves | 10/10 | 10/10 | parity |
| Total tokens | 1,624,930 | 1,430,032 | 11.99% |
| MCP result bytes | 48,785 | 31,375 | 35.69% |
The comparison includes MCP schema and tool-call overhead. See the checked-in agentic benchmark report for task-level results, cache sensitivity, integrity checks, and limitations, or the TOON comparison report for the format-specific run. See the agentic benchmark methodology to reproduce it.
Provider-neutral parsing and the standalone CLI live in
ewhauser/logcompact. This repository
depends on logcompact-builtins 0.3.3 for deterministic compiler and test-log
parsing; Bazel-specific path, event, and message semantics remain in
bazel-mcp-reducer.
cargo install logcompact --version 0.3.3
logcompact build.log --format humanContributions are welcome. The usual local checks are:
make build
make test
make checkRunner, BEP, policy, or reducer changes should also run
make test-bazel-matrix. Reducer changes should additionally run
make test-reducer-corpus and follow the
integration case workflow. Read
CONTRIBUTING.md before opening a pull request.
Use make bench-reducers to compare the same diagnostic reducer implemented in
native Rust and Starlark. See the
Starlark reducer performance report for
the checked-in measurements and interpretation.
Generic parser benchmarks are maintained with the parser implementation in the
logcompact repository.
Architecture and protocol decisions are recorded under specs/.
bazel-mcp is available under the MIT License.