Install
$ agentstack add mcp-ewhauser-bazel-mcp ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
bazel-mcp
[](https://github.com/ewhauser/bazel-mcp/actions/workflows/ci.yml) [](https://ewhauser.github.io/bazel-mcp/) [](LICENSE)
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](#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](docs/configuration.md#enable-bazel-run) 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](specs/003-configurable-mcp-task-execution.md) 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, 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](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](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](docs/bep-transport-performance.md) 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](docs/configuration.md) 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](docs/custom-reducers.md) 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 | Supporte
…
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: ewhauser
- Source: ewhauser/bazel-mcp
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.