# Directory Structure
```
├── .gitignore
├── index.ts
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Dependency directories
node_modules/
dist/
# TypeScript
*.tsbuildinfo
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Build output
lib/
build/
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea/
.vscode/
*.swp
*.swo
# Operating System Files
.DS_Store
Thumbs.db
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Bazel MCP Server
A local MCP server that exposes functionality of the [Bazel](https://bazel.build/) build system to MCP-enabled AI agents.
This is helpful when MCP environments either don't have an existing command-line tool, or where the invoked shell has a misconfigured environment that prevents Bazel from being used.
## Tools
The Bazel MCP Server provides the following tools:
- **bazel_build_target**: Build specified Bazel targets
- **bazel_query_target**: Query the dependency graph for targets matching a pattern
- **bazel_test_target**: Run tests for specified targets
- **bazel_list_targets**: List all available targets in the workspace (requires path parameter, use "//" for all targets)
- **bazel_fetch_dependencies**: Fetch external dependencies
- **bazel_set_workspace_path**: Change the Bazel workspace path at runtime
Each command (except `bazel_set_workspace_path`) supports an optional `additionalArgs` parameter that allows passing additional arguments to the underlying Bazel command. This is useful for specifying flags like `--verbose_failures` or `--test_output=all`.
## Usage
### Installation
#### Using with Cursor
Add the following to `.cursor/mcp.json`.
You don't need to provide the workspace path, as the LLM can use `set_workspace_path` to change the workspace path at runtime.
The bazel binary usually gets picked up automatically, but if you run into issues, you can provide the path to the bazel binary using the `--bazel_path` flag.
> :warning: Note that this will not work when using Cursor with Remote SSH sessions, since it runs the MCP server locally.
```json
{
"mcpServers": {
"bazel": {
"command": "npx",
"args": [
"-y",
"github:nacgarg/bazel-mcp-server",
// If you need to specify the bazel binary path
"--bazel_path",
"/absolute/path/to/your/bazel/binary",
// If you need to specify the workspace path
"--workspace_path",
"/absolute/path/to/your/bazel/workspace"
// See Configuration Table below for more options
]
}
}
}
```
#### Using with Claude Desktop
You can use the same configuration as above with Claude Desktop.
#### Launching standalone
```bash
# Run directly from GitHub (no installation needed)
npx -y github:nacgarg/bazel-mcp-server
# From source
git clone https://github.com/nacgarg/bazel-mcp-server.git
cd bazel-mcp-server
npm install
npm run build
dist/index.js
```
### Configuration
This MCP server allows a couple different configuration methods. They will be used in the following order:
1. Command line arguments
2. Environment variables
3. Configuration file
### Configuration Table
| CLI Argument | Environment Variable | Configuration File Key | Description |
|--------------|----------------------|------------------------|-------------|
| `--bazel_path` | `MCP_BAZEL_PATH` | `bazel_path` | The path to the Bazel binary to use. |
| `--workspace_path` | `MCP_WORKSPACE_PATH` | `workspace_path` | The path to the Bazel workspace to use. |
| `--workspace_config` | `MCP_WORKSPACE_CONFIG` | `workspace_config` | The configuration of the workspace to use. By default, this uses the `.bazelrc` file in the workspace root. |
| `--log_path` | `MCP_LOG_PATH` | `log_path` | The path to write server logs to. |
## Debugging
Set the `DEBUG=true` environment variable to enable verbose logging to the console.
Setting the log path is also helpful for debugging with clients that don't print logs to the console (looking at you, Cursor).
## License
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"outDir": "./dist",
"rootDir": "."
},
"include": ["index.ts"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "@nacgarg/bazel-mcp-server",
"version": "0.1.0",
"description": "MCP server for interacting with Bazel",
"license": "MIT",
"author": "nacgarg",
"homepage": "https://github.com/nacgarg/bazel-mcp-server",
"repository": {
"type": "git",
"url": "https://github.com/nacgarg/bazel-mcp-server.git"
},
"bugs": "https://github.com/nacgarg/bazel-mcp-server/issues",
"type": "module",
"bin": {
"bazel-mcp-server": "dist/index.js"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc && shx chmod +x dist/*.js",
"prepare": "npm run build",
"watch": "tsc --watch",
"lint": "eslint --ext .ts ."
},
"dependencies": {
"@modelcontextprotocol/sdk": "1.0.1"
},
"devDependencies": {
"@types/node": "^22",
"shx": "^0.3.4",
"typescript": "^5.6.2"
},
"engines": {
"node": ">=18"
},
"keywords": [
"bazel",
"model-context-protocol",
"mcp",
"cli"
]
}
```
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequest,
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import { spawn } from "child_process";
import fs from "fs";
import path from "path";
// Logger configuration
let logPath = '';
/**
* Centralized logging function that handles both console and file logging
*/
function log(message: string, level: 'info' | 'error' = 'info', toConsole = true): void {
const timestamp = new Date().toISOString();
const logMessage = `${timestamp}: ${message}`;
// Log to file if path is configured
if (logPath) {
fs.appendFileSync(logPath, `${logMessage}\n`);
}
// Log to console if requested
if (toConsole) {
if (level === 'error') {
console.error(logMessage);
} else if (process.env.DEBUG) {
console.error(logMessage); // Debug logs also go to stderr
}
}
}
// Type definitions for tool arguments
interface BuildTargetArgs {
targets: string[];
additionalArgs?: string[];
}
interface QueryTargetArgs {
pattern: string;
additionalArgs?: string[];
}
interface TestTargetArgs {
targets: string[];
additionalArgs?: string[];
}
interface ListTargetsArgs {
path: string;
additionalArgs?: string[];
}
interface FetchDependenciesArgs {
targets?: string[];
additionalArgs?: string[];
}
interface SetWorkspacePathArgs {
path: string;
}
// Tool definitions
const buildTargetTool: Tool = {
name: "bazel_build_target",
description: "Build specified Bazel targets",
inputSchema: {
type: "object",
properties: {
targets: {
type: "array",
items: {
type: "string",
},
description: "List of Bazel targets to build (e.g. ['//path/to:target'])",
},
additionalArgs: {
type: "array",
items: {
type: "string",
},
description: "Additional Bazel command line arguments (e.g. ['--verbose_failures', '--sandbox_debug'])",
},
},
required: ["targets"],
},
};
const queryTargetTool: Tool = {
name: "bazel_query_target",
description: "Query the Bazel dependency graph for targets matching a pattern",
inputSchema: {
type: "object",
properties: {
pattern: {
type: "string",
description: "Bazel query pattern (e.g. 'deps(//path/to:target)')",
},
additionalArgs: {
type: "array",
items: {
type: "string",
},
description: "Additional Bazel command line arguments (e.g. ['--output=label_kind', '--noimplicit_deps'])",
},
},
required: ["pattern"],
},
};
const testTargetTool: Tool = {
name: "bazel_test_target",
description: "Run Bazel tests for specified targets",
inputSchema: {
type: "object",
properties: {
targets: {
type: "array",
items: {
type: "string",
},
description: "List of Bazel test targets to run (e.g. ['//path/to:test'])",
},
additionalArgs: {
type: "array",
items: {
type: "string",
},
description: "Additional Bazel command line arguments (e.g. ['--cache_test_results=no', '--test_output=all'])",
},
},
required: ["targets"],
},
};
const listTargetsTool: Tool = {
name: "bazel_list_targets",
description: "List all available Bazel targets under a given path",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "Path within the workspace to list targets for (e.g. '//path/to' or '//' for all targets)",
},
additionalArgs: {
type: "array",
items: {
type: "string",
},
description: "Additional Bazel command line arguments (e.g. ['--output=build', '--keep_going'])",
},
},
required: ["path"],
},
};
const fetchDependenciesTool: Tool = {
name: "bazel_fetch_dependencies",
description: "Fetch Bazel external dependencies",
inputSchema: {
type: "object",
properties: {
targets: {
type: "array",
items: {
type: "string",
},
description: "List of specific targets to fetch dependencies for",
},
additionalArgs: {
type: "array",
items: {
type: "string",
},
description: "Additional Bazel command line arguments (e.g. ['--experimental_repository_cache_hardlinks', '--repository_cache=path/to/cache'])",
},
},
},
};
const setWorkspacePathTool: Tool = {
name: "bazel_set_workspace_path",
description: "Set the current Bazel workspace path for subsequent commands",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "The absolute path to the Bazel workspace directory",
},
},
required: ["path"],
},
};
class BazelClient {
private bazelPath: string;
private workspacePath: string;
private workspaceConfig: string | undefined;
// List of allowed prefixes for additional arguments to prevent command injection
private readonly allowedArgPrefixes = ['--', '-'];
constructor(
bazelPath: string,
workspacePath: string,
workspaceConfig?: string
) {
this.bazelPath = bazelPath;
this.workspacePath = workspacePath;
this.workspaceConfig = workspaceConfig;
}
/**
* Validates and sanitizes additional Bazel arguments to prevent command injection
* @param args Array of additional arguments to validate
* @returns Array of validated and sanitized arguments
* @throws Error if any argument is invalid or potentially dangerous
*/
private validateAdditionalArgs(args: string[] | undefined): string[] {
if (!args || args.length === 0) {
return [];
}
const sanitizedArgs: string[] = [];
for (const arg of args) {
// Skip empty arguments
if (!arg || arg.trim() === '') {
continue;
}
// Check if argument starts with allowed prefix
const isAllowed = this.allowedArgPrefixes.some(prefix => arg.startsWith(prefix));
if (!isAllowed) {
throw new Error(`Invalid argument format: "${arg}". Additional arguments must start with -- or -`);
}
// Check for potentially dangerous characters that could enable command injection
const dangerousChars = /[;&|<>$`\\]/;
if (dangerousChars.test(arg)) {
throw new Error(`Argument contains potentially dangerous characters: "${arg}"`);
}
sanitizedArgs.push(arg);
}
return sanitizedArgs;
}
private runBazelCommand(
command: string,
args: string[] = [],
onOutput?: (chunk: string) => void
): Promise<{ stdout: string; stderr: string }> {
return new Promise((resolve, reject) => {
const fullArgs = [command, ...args];
if (this.workspaceConfig) {
fullArgs.unshift(`--bazelrc=${this.workspaceConfig}`);
}
const cmdString = `${this.bazelPath} ${fullArgs.join(" ")}`;
log(`Running command: ${cmdString} in directory: ${this.workspacePath}`);
const process = spawn(this.bazelPath, fullArgs, {
cwd: this.workspacePath,
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
process.stdout.on("data", (data) => {
const chunk = data.toString();
stdout += chunk;
log(`STDOUT: ${chunk}`, 'info', false);
if (onOutput) {
onOutput(chunk);
}
});
process.stderr.on("data", (data) => {
const chunk = data.toString();
stderr += chunk;
log(`STDERR: ${chunk}`, 'info', false);
if (onOutput) {
onOutput(chunk);
}
});
process.on("close", (code) => {
log(`Command completed with exit code: ${code}`);
if (code === 0) {
resolve({ stdout, stderr });
} else {
const errorMsg = `Bazel command failed with code ${code}: ${stderr}`;
log(errorMsg, 'error');
reject(new Error(errorMsg));
}
});
process.on("error", (err) => {
log(`Command execution error: ${err.message}`, 'error');
reject(err);
});
});
}
async buildTargets(targets: string[], additionalArgs?: string[], onOutput?: (chunk: string) => void): Promise<string> {
const validatedArgs = this.validateAdditionalArgs(additionalArgs);
const allArgs = [...targets, ...validatedArgs];
const { stdout, stderr } = await this.runBazelCommand("build", allArgs, onOutput);
return `${stdout}\n${stderr}`;
}
async queryTarget(pattern: string, additionalArgs?: string[], onOutput?: (chunk: string) => void): Promise<string> {
const validatedArgs = this.validateAdditionalArgs(additionalArgs);
const allArgs = [pattern, ...validatedArgs];
const { stdout, stderr } = await this.runBazelCommand("query", allArgs, onOutput);
return stdout || stderr;
}
async testTargets(targets: string[], additionalArgs?: string[], onOutput?: (chunk: string) => void): Promise<string> {
const validatedArgs = this.validateAdditionalArgs(additionalArgs);
const allArgs = [...targets, ...validatedArgs];
const { stdout, stderr } = await this.runBazelCommand("test", allArgs, onOutput);
return `${stdout}\n${stderr}`;
}
async listTargets(path: string, additionalArgs?: string[], onOutput?: (chunk: string) => void): Promise<string> {
const queryPattern = `${path}/...`;
const validatedArgs = this.validateAdditionalArgs(additionalArgs);
const allArgs = [queryPattern, ...validatedArgs];
const { stdout } = await this.runBazelCommand("query", allArgs, onOutput);
return stdout;
}
async fetchDependencies(targets?: string[], additionalArgs?: string[], onOutput?: (chunk: string) => void): Promise<string> {
const validatedArgs = this.validateAdditionalArgs(additionalArgs);
const args = ["fetch"];
if (targets && targets.length > 0) {
args.push(...targets);
} else {
args.push("//...");
}
args.push(...validatedArgs);
const { stdout, stderr } = await this.runBazelCommand("build", args, onOutput);
return `${stdout}\n${stderr}`;
}
setWorkspacePath(newPath: string): string {
if (!fs.existsSync(newPath)) {
throw new Error(`Workspace path does not exist: ${newPath}`);
}
// Check if it appears to be a Bazel workspace
const isWorkspace = fs.existsSync(path.join(newPath, 'WORKSPACE')) ||
fs.existsSync(path.join(newPath, 'WORKSPACE.bazel')) ||
fs.existsSync(path.join(newPath, 'MODULE.bazel'));
if (!isWorkspace) {
throw new Error(`Path does not appear to be a Bazel workspace: ${newPath}`);
}
const oldPath = this.workspacePath;
this.workspacePath = newPath;
return `Workspace path updated from ${oldPath} to ${newPath}`;
}
}
// Parse CLI arguments and environment variables
function getConfig() {
const args = process.argv.slice(2);
const config: {
bazelPath: string;
workspacePath: string;
workspaceConfig?: string;
logPath: string;
} = {
bazelPath: "bazel",
workspacePath: process.cwd(),
logPath: ""
};
// Parse command line arguments
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === "--bazel_path" && i + 1 < args.length) {
config.bazelPath = args[++i];
} else if (arg === "--workspace_path" && i + 1 < args.length) {
config.workspacePath = args[++i];
} else if (arg === "--workspace_config" && i + 1 < args.length) {
config.workspaceConfig = args[++i];
} else if (arg === "--log_path" && i + 1 < args.length) {
config.logPath = args[++i];
}
}
// Override with environment variables if set
if (process.env.MCP_BAZEL_PATH) {
config.bazelPath = process.env.MCP_BAZEL_PATH;
}
if (process.env.MCP_WORKSPACE_PATH) {
config.workspacePath = process.env.MCP_WORKSPACE_PATH;
}
if (process.env.MCP_WORKSPACE_CONFIG) {
config.workspaceConfig = process.env.MCP_WORKSPACE_CONFIG;
}
if (process.env.MCP_LOG_PATH) {
config.logPath = process.env.MCP_LOG_PATH;
}
// Check for config file
const configFilePath = path.resolve(process.cwd(), ".bazel-mcp-config.json");
if (fs.existsSync(configFilePath)) {
try {
const fileConfig = JSON.parse(fs.readFileSync(configFilePath, "utf-8"));
if (!process.env.MCP_BAZEL_PATH && !args.includes("--bazel_path") && fileConfig.bazel_path) {
config.bazelPath = fileConfig.bazel_path;
}
if (!process.env.MCP_WORKSPACE_PATH && !args.includes("--workspace_path") && fileConfig.workspace_path) {
config.workspacePath = fileConfig.workspace_path;
}
if (!process.env.MCP_WORKSPACE_CONFIG && !args.includes("--workspace_config") && fileConfig.workspace_config) {
config.workspaceConfig = fileConfig.workspace_config;
}
if (!process.env.MCP_LOG_PATH && !args.includes("--log_path") && fileConfig.log_path) {
config.logPath = fileConfig.log_path;
}
} catch (error) {
console.error("Error reading config file:", error);
}
}
// Update the global log path
logPath = config.logPath;
return config;
}
async function main() {
const config = getConfig();
// Server startup
log(`Server starting. PWD: ${process.cwd()}`);
if (logPath) {
log(`Log path configured: ${logPath}`);
}
log("Starting Bazel MCP Server...", 'info', true);
log(`Using Bazel at: ${config.bazelPath}`);
log(`Workspace path: ${config.workspacePath}`);
if (config.workspaceConfig) {
log(`Workspace config: ${config.workspaceConfig}`);
}
// Debug info (only logged to file)
log(`Environment variables: ${JSON.stringify(process.env)}`, 'info', false);
log(`Command line arguments: ${JSON.stringify(process.argv)}`, 'info', false);
try {
// Check if we're in a Bazel workspace
const workspaceExists = fs.existsSync(path.join(config.workspacePath, 'WORKSPACE')) ||
fs.existsSync(path.join(config.workspacePath, 'WORKSPACE.bazel')) ||
fs.existsSync(path.join(config.workspacePath, 'MODULE.bazel'));
log(`Is Bazel workspace: ${workspaceExists}`);
if (!workspaceExists) {
log(`Warning: ${config.workspacePath} does not appear to be a Bazel workspace`, 'error');
}
} catch (err) {
log(`Error checking workspace: ${err}`, 'error');
}
const server = new Server(
{
name: "Bazel MCP Server",
version: "0.1.0",
},
{
capabilities: {
tools: {},
},
},
);
const bazelClient = new BazelClient(
config.bazelPath,
config.workspacePath,
config.workspaceConfig
);
server.setRequestHandler(
CallToolRequestSchema,
async (request: CallToolRequest) => {
log(`Received CallToolRequest for tool: ${request.params.name}`, 'info', process.env.DEBUG === 'true');
try {
if (!request.params.arguments) {
throw new Error("No arguments provided");
}
let response;
switch (request.params.name) {
case "bazel_build_target": {
const args = request.params.arguments as unknown as BuildTargetArgs;
log(`Processing bazel_build_target with args: ${JSON.stringify(args)}`, 'info', false);
if (!args.targets || args.targets.length === 0) {
throw new Error("Missing required argument: targets");
}
response = await bazelClient.buildTargets(args.targets, args.additionalArgs);
break;
}
case "bazel_query_target": {
const args = request.params.arguments as unknown as QueryTargetArgs;
log(`Processing bazel_query_target with pattern: ${args.pattern}`, 'info', false);
if (!args.pattern) {
throw new Error("Missing required argument: pattern");
}
response = await bazelClient.queryTarget(args.pattern, args.additionalArgs);
break;
}
case "bazel_test_target": {
const args = request.params.arguments as unknown as TestTargetArgs;
log(`Processing bazel_test_target with args: ${JSON.stringify(args)}`, 'info', false);
if (!args.targets || args.targets.length === 0) {
throw new Error("Missing required argument: targets");
}
response = await bazelClient.testTargets(args.targets, args.additionalArgs);
break;
}
case "bazel_list_targets": {
const args = request.params.arguments as unknown as ListTargetsArgs;
log(`Processing bazel_list_targets for path: ${args.path}`, 'info', false);
if (!args.path) {
throw new Error("Missing required argument: path");
}
response = await bazelClient.listTargets(args.path, args.additionalArgs);
break;
}
case "bazel_fetch_dependencies": {
const args = request.params.arguments as unknown as FetchDependenciesArgs;
log(`Processing bazel_fetch_dependencies`, 'info', false);
response = await bazelClient.fetchDependencies(args.targets, args.additionalArgs);
break;
}
case "bazel_set_workspace_path": {
const args = request.params.arguments as unknown as SetWorkspacePathArgs;
log(`Processing bazel_set_workspace_path to: ${args.path}`, 'info', false);
if (!args.path) {
throw new Error("Missing required argument: path");
}
response = bazelClient.setWorkspacePath(args.path);
break;
}
default:
throw new Error(`Unknown tool: ${request.params.name}`);
}
const result = {
content: [{ type: "text", text: response }],
};
log(`Tool execution completed successfully`, 'info', false);
return result;
} catch (error) {
log(`Error executing tool: ${error instanceof Error ? error.message : String(error)}`, 'error');
return {
content: [
{
type: "text",
text: JSON.stringify({
error: error instanceof Error ? error.message : String(error),
}),
},
],
};
}
},
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
log("Received ListToolsRequest", 'info', process.env.DEBUG === 'true');
const response = {
tools: [
buildTargetTool,
queryTargetTool,
testTargetTool,
listTargetsTool,
fetchDependenciesTool,
setWorkspacePathTool,
],
};
log(`Sending ListToolsResponse with ${response.tools.length} tools`, 'info', false);
return response;
});
const transport = new StdioServerTransport();
log("Connecting server to transport...", 'info', true);
await server.connect(transport);
log("Bazel MCP Server running on stdio", 'info', true);
}
main().catch((error) => {
log(`FATAL ERROR: ${error.message}`, 'error');
log(`Stack trace: ${error.stack || 'No stack trace available'}`, 'error', false);
process.exit(1);
});
```