Skip to content

ewhauser/bazel-mcp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

219 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

bazel-mcp

CI Documentation License: MIT

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.

Why bazel-mcp?

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.

Quick start

Requirements

  • 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)

Install with Homebrew

brew install ewhauser/tap/bazel-mcp

Install from a release

Download 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 | sh

On Windows, run the PowerShell installer:

irm https://github.com/ewhauser/bazel-mcp/releases/latest/download/bazel-mcp-server-installer.ps1 | iex

Build from source

Clone 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-mcp

The binary is written to bazel-bin/crates/bazel-mcp-server/bazel-mcp.

Connect your MCP client

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.

Use the filtered Bazel CLI

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.

Tools

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 protocol shape

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

Synchronous call

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.

Legacy MCP tasks

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.

Tasks extension

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.

Security and local data

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, and sync by 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.

Configuration

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.

Custom reducers

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.

Result formats

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.

Compatibility

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.

Performance

Deterministic invocation benchmark

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.

Agentic coding benchmark

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.

Log reduction

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 human

Contributing

Contributions are welcome. The usual local checks are:

make build
make test
make check

Runner, 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/.

License

bazel-mcp is available under the MIT License.

About

MCP server for Bazel focused on token reduction

Resources

License

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors