A Model Context Protocol (MCP) server and CLI for Bazel-based Apple platform development. Ships 125 tools across 21 workflow categories covering iOS, macOS, tvOS, watchOS, visionOS, and Swift Package Manager.
- Build & Run — Build, install, launch, and stop iOS/macOS apps on simulators and physical devices.
- Test & Coverage — Run unit/UI/build tests with filtering, streaming output, and code coverage.
- Simulator Management — Boot, shutdown, erase, configure location/appearance/status bar.
- Physical Device — Full device lifecycle via
xcrun devicectl: list, pair, install, launch, terminate, screenshot, log capture. - LLDB Debugging — Attach to simulator or device processes, set breakpoints, inspect variables, step through code.
- UI Automation — Tap, swipe, type, drag, pinch, and inspect accessibility trees on simulators (via IDB or CGEvent fallback).
- Multi-platform — tvOS, watchOS, visionOS build/run/test/discover targets.
- Swift Package Manager — Build, test, run, clean, resolve, dump, and init Swift packages.
- Project Discovery — Query the Bazel build graph, discover targets, inspect dependencies.
- Scaffolding — Generate new Bazel projects from templates (ios_app, macos_app, etc.).
- Session & Config — Workspace management, build profiles, defaults, health checks.
- Background Daemon — Per-workspace daemon for stateful operations.
- Self-update — Check for and install updates.
- macOS with Xcode installed
- Node.js 18+
- Bazel or Bazelisk on
PATH - A Bazel workspace (set via
BAZEL_IOS_WORKSPACE, config file, orset_workspacetool)
pymobiledevice3—pip3 install pymobiledevice3- For screenshots:
sudo pymobiledevice3 remote tunneldrunning in background - For logs: pymobiledevice3 is tried first automatically, falls back to
idevicesyslog
npm install -g xcodebazelmcpThen use the installed CLI:
xcodebazelmcp mcpbrew install XcodeBazelMCP/tap/xcodebazelmcpThen use the installed CLI:
xcodebazelmcp mcpOr run directly via npx (no install needed):
npx -y xcodebazelmcp mcpnpx -y xcodebazelmcp setupnpx -y xcodebazelmcp init{
"mcpServers": {
"XcodeBazelMCP": {
"command": "npx",
"args": ["-y", "xcodebazelmcp", "mcp"],
"env": {
"BAZEL_IOS_WORKSPACE": "/path/to/your/ios-workspace"
}
}
}
}If you installed the CLI globally with npm or Homebrew:
{
"mcpServers": {
"XcodeBazelMCP": {
"command": "xcodebazelmcp",
"args": ["mcp"],
"env": {
"BAZEL_IOS_WORKSPACE": "/path/to/your/ios-workspace"
}
}
}
}Or with workspace flag:
npx -y xcodebazelmcp mcp --workspace /path/to/your/ios-workspacegit clone https://github.com/XcodeBazelMCP/XcodeBazelMCP.git
cd XcodeBazelMCP
npm install
npm run buildXcodeBazelMCP needs to know which Bazel workspace to operate on. There are several ways to set it, in order of precedence:
- MCP tool at runtime —
bazel_ios_set_workspace(sets the workspace for later tool calls) - CLI flag —
xcodebazelmcp mcp --workspace /path/to/workspace - Environment variable —
BAZEL_IOS_WORKSPACE=/path/to/workspace - Config file —
.xcodebazelmcp/config.yamlin the workspace root (supports profiles) - Fallback —
process.cwd()
For multi-workspace setups, use profiles in config.yaml:
profiles:
app:
defaultTarget: '//app:app'
defaultPlatform: simulator
defaultBuildMode: debug
mac:
defaultTarget: '//mac:mac'
defaultPlatform: macosThen switch at runtime: xcodebazelmcp set-defaults --profile app
| Variable | Purpose |
|---|---|
BAZEL_IOS_WORKSPACE |
Bazel workspace root (highest-precedence workspace source). |
BAZEL_PATH / MCP_BAZEL_PATH |
Path to the Bazel/Bazelisk binary (defaults to bazel on PATH). |
BAZEL_IOS_STARTUP_ARGS |
Bazel startup args prepended to every invocation (quote-aware). |
BAZEL_IOS_MCP_MAX_OUTPUT |
Max captured command output in characters (default 200000). |
BAZEL_IOS_SIMULATOR_CPU |
Override the iOS simulator CPU (sim_arm64 / x86_64; default: host). |
BAZEL_IOS_DISCOVER_SCOPE |
Default discover query scope for non-monorepo workspaces (e.g. //...). |
BAZEL_IOS_COMMAND_LOG |
Path to the persistent NDJSON command log. |
BAZEL_IOS_COMMAND_LOG_MAX_BYTES |
Rotate the command log past this size (default 5 MiB). |
BAZEL_IOS_COMMAND_LOG_DISABLE |
Set to 1 to disable command logging. |
IDB_PATH |
Path to the idb binary for UI automation (else discovered on PATH). |
BITRISE_BUILD_CACHE_AUTH_TOKEN |
Not required. Local builds/tests are the default. Set this only when you intentionally want Bitrise RBE (--config=bitrise / --config=remote_linux). Without it, those configs are dropped automatically. |
DoorDash's ios repo defines --config=bitrise and --config=remote_linux for optional Bitrise remote execution. You do not need BITRISE_BUILD_CACHE_AUTH_TOKEN for XcodeBazelMCP. Agents and tools run locally on your Mac by default.
If a call passes --config=bitrise or --config=remote_linux without the token, XcodeBazelMCP silently drops those flags and runs locally. Set the token only when you explicitly want RBE offload (e.g. heavy compiles on Bitrise executors).
# Health check
xcodebazelmcp doctor
# Discover & query
xcodebazelmcp discover --scope //Apps/... --kind apps
xcodebazelmcp query 'deps(//app:app)'
# Build & run (simulator)
xcodebazelmcp build //app:app --debug --simulator
xcodebazelmcp run //app:app --simulator-name "iPhone 16 Pro"
# Build & run (device)
xcodebazelmcp device-run //app:app --device-name "iPhone"
xcodebazelmcp device-screenshot output.png --device-name "iPhone"
xcodebazelmcp device-log-start --device-name "iPhone"
# Test
xcodebazelmcp test //tests:UnitTests --filter "SomeTest/testCase" --stream
# macOS
xcodebazelmcp macos-build //mac:mac --debug
xcodebazelmcp macos-run //mac:mac
# Swift Package Manager
xcodebazelmcp spm-build --path ./MyPackage
xcodebazelmcp spm-test --filter "MyTests/testExample"
# Scaffold
xcodebazelmcp new ios_app MyNewApp --bundle-id com.example.MyNewApp
# Config
xcodebazelmcp defaults
xcodebazelmcp set-defaults --target //app:app --simulator-name "iPhone 16 Pro"
xcodebazelmcp workflowsWorkflows control which tools are advertised to MCP clients. Smart defaults enable the most common workflows; use toggle-workflow to customize.
| Category | Tools | Description |
|---|---|---|
| build | 2 | Build iOS targets for simulator or device |
| test | 2 | Run iOS tests with optional coverage |
| simulator | 12 | Manage simulator lifecycle, settings, media, and containers |
| app_lifecycle | 6 | Install, launch, stop, uninstall apps on simulator |
| capture | 5 | Screenshot, video recording, log capture (simulator) |
| agent_debug | 4 | NDJSON agent debug logs (Cursor DEBUG MODE) |
| ui_automation | 9 | Tap, swipe, type, drag, accessibility snapshot |
| deep_links | 2 | Open URLs and send push notifications |
| device | 16 | Physical device build, deploy, test, screenshot, logs, uninstall, list apps |
| lldb | 10 | LLDB debugger: breakpoints, variables, stepping |
| macos | 13 | macOS build, run, test, discover |
| tvos | 4 | tvOS build, run, test, discover |
| watchos | 4 | watchOS build, run, test, discover |
| visionos | 4 | visionOS build, run, test, discover |
| spm | 7 | Swift Package Manager operations |
| project | 6 | Target discovery, query, deps, rdeps |
| scaffold | 2 | Generate new Bazel projects |
| session | 7 | Workspace, defaults, profiles, health |
| daemon | 3 | Background daemon management |
| update | 2 | Self-update and version check |
| xcode | 3 | Apple-native Xcode MCP detection, DeviceHub, skills (docs) |
Apple replaced lockdownd/usbmuxd with CoreDevice (remoted) starting in iOS 17 / macOS 15. This means:
xcrun devicectlis the only reliable CLI for device operations (list, install, launch, terminate).- Screenshots require
pymobiledevice3withtunneldrunning — nodevicectlscreenshot subcommand exists. - Logs use
pymobiledevice3 syslog live(automatic fallback toidevicesyslogfor older devices). - Process termination uses a two-step PID lookup since
devicectldoesn't exposebundleIdentifierin process listings.
See docs/device-support.md for full details.
git clone https://github.com/XcodeBazelMCP/XcodeBazelMCP.git
cd XcodeBazelMCP
npm install
npm run build # Build dist/
npm test # Run tests (vitest)
npm run lint # ESLintThe server launches Bazel with spawn, not a shell. Extra flags are passed as argv entries so commands are reproducible and avoid shell interpolation.