Skip to content

Commit 713f0bf

Browse files
Harden MCP tooling: correctness fixes, validation, and new sim/device tools
A reliability/consistency pass across the MCP tools and CLI, plus new simulator/device tools (tool count 120 -> 125). All changes verified against a real Bazel iOS workspace. Highlights: - Fix combined stdout/stderr JSON parsing (simctl list, swift dump-package, idb accessibility snapshot) - Host-arch-aware iOS simulator CPU (BAZEL_IOS_SIMULATOR_CPU); quote-aware BAZEL_IOS_STARTUP_ARGS; env-overridable discover scope - LLDB: per-command output buffer (fixes >500KB deadlock), newline/quote injection guards, session cleanup on shutdown - Streaming tools return structured content; Bazel exit-code annotations - Scaffold: bundleId/minimumOs/rulesVersion validation, families & bazelVersion options, iOS Info.plist keys, absolute config path - New tools: bazel_ios_uninstall_app, bazel_ios_device_uninstall_app, bazel_ios_device_list_apps, bazel_ios_add_media, bazel_ios_get_app_container - Doctor reports idb/pymobiledevice3/maxOutput; README env-var section - Runtime smoke script + CI smoke step; +45 tests (575 -> 619 passing) Note: also snapshots in-progress xcode-native-mcp, command-log, and diagnostics modules already present in the working tree. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent a335b17 commit 713f0bf

64 files changed

Lines changed: 3635 additions & 514 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ jobs:
5151
- name: Build
5252
run: npm run build
5353

54+
- name: Smoke (CLI boot, tool inventory, scaffold)
55+
run: npm run smoke
56+
5457
tag:
5558
name: Bump version & tag
5659
needs: [ci, build]

README.md

Lines changed: 57 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,21 @@
55

66
<p>
77
A Model Context Protocol (MCP) server and CLI for Bazel-based Apple platform development.
8-
Ships 112 tools across 19 workflow categories covering iOS, macOS, tvOS, watchOS,
8+
Ships 125 tools across 21 workflow categories covering iOS, macOS, tvOS, watchOS,
99
visionOS, and Swift Package Manager.
1010
</p>
1111

12-
[![CI](https://github.com/XcodeBazelMCP/XcodeBazelMCP/actions/workflows/ci.yml/badge.svg)](https://github.com/XcodeBazelMCP/XcodeBazelMCP/actions/workflows/ci.yml)
13-
[![npm package](https://img.shields.io/npm/v/xcodebazelmcp)](https://www.npmjs.com/package/xcodebazelmcp)
14-
[![License MIT](https://img.shields.io/npm/l/xcodebazelmcp)](https://github.com/XcodeBazelMCP/XcodeBazelMCP/blob/main/LICENSE)
15-
[![node >=18.x](https://img.shields.io/node/v/xcodebazelmcp)](https://nodejs.org/)
16-
[![Xcode 16](https://img.shields.io/badge/Xcode-16-blue)](https://developer.apple.com/xcode/)
17-
[![platform macOS](https://img.shields.io/badge/platform-macOS-lightgrey)](https://github.com/XcodeBazelMCP/XcodeBazelMCP)
18-
[![MCP Compatible](https://img.shields.io/badge/MCP-Compatible-green)](https://modelcontextprotocol.io)
19-
[![codecov](https://codecov.io/gh/XcodeBazelMCP/XcodeBazelMCP/branch/main/graph/badge.svg)](https://codecov.io/gh/XcodeBazelMCP/XcodeBazelMCP)
20-
[![Socket Badge](https://badge.socket.dev/npm/package/xcodebazelmcp/0.1.0)](https://badge.socket.dev/npm/package/xcodebazelmcp/0.1.0)
21-
[![Ask DeepWiki](https://img.shields.io/badge/Ask-DeepWiki-blue?logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bTEgMTVoLTJ2LTJoMnYyem0wLTRoLTJWN2gydjZ6IiBmaWxsPSJ3aGl0ZSIvPjwvc3ZnPg==)](https://deepwiki.com/XcodeBazelMCP/XcodeBazelMCP)
12+
[![CI](https://github.com/XcodeBazelMCP/XcodeBazelMCP/actions/workflows/ci.yml/badge.svg)](https://github.com/XcodeBazelMCP/XcodeBazelMCP/actions/workflows/ci.yml)
13+
[![npm package](https://img.shields.io/npm/v/xcodebazelmcp)](https://www.npmjs.com/package/xcodebazelmcp)
14+
[![License MIT](https://img.shields.io/npm/l/xcodebazelmcp)](https://github.com/XcodeBazelMCP/XcodeBazelMCP/blob/main/LICENSE)
15+
[![node >=18.x](https://img.shields.io/node/v/xcodebazelmcp)](https://nodejs.org/)
16+
[![Xcode 16](https://img.shields.io/badge/Xcode-16-blue)](https://developer.apple.com/xcode/)
17+
[![platform macOS](https://img.shields.io/badge/platform-macOS-lightgrey)](https://github.com/XcodeBazelMCP/XcodeBazelMCP)
18+
[![MCP Compatible](https://img.shields.io/badge/MCP-Compatible-green)](https://modelcontextprotocol.io)
19+
[![codecov](https://codecov.io/gh/XcodeBazelMCP/XcodeBazelMCP/branch/main/graph/badge.svg)](https://codecov.io/gh/XcodeBazelMCP/XcodeBazelMCP)
20+
[![Socket Badge](https://badge.socket.dev/npm/package/xcodebazelmcp/0.1.0)](https://badge.socket.dev/npm/package/xcodebazelmcp/0.1.0)
21+
[![Ask DeepWiki](https://img.shields.io/badge/Ask-DeepWiki-blue?logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bTEgMTVoLTJ2LTJoMnYyem0wLTRoLTJWN2gydjZ6IiBmaWxsPSJ3aGl0ZSIvPjwvc3ZnPg==)](https://deepwiki.com/XcodeBazelMCP/XcodeBazelMCP)
22+
2223
</div>
2324

2425
<br>
@@ -147,7 +148,7 @@ npm run build
147148

148149
XcodeBazelMCP needs to know which Bazel workspace to operate on. There are several ways to set it, in order of precedence:
149150

150-
1. **MCP tool at runtime**`bazel_ios_set_workspace` (or CLI `set-defaults --target //app:app`)
151+
1. **MCP tool at runtime**`bazel_ios_set_workspace` (sets the workspace for later tool calls)
151152
2. **CLI flag**`xcodebazelmcp mcp --workspace /path/to/workspace`
152153
3. **Environment variable**`BAZEL_IOS_WORKSPACE=/path/to/workspace`
153154
4. **Config file**`.xcodebazelmcp/config.yaml` in the workspace root (supports profiles)
@@ -158,16 +159,31 @@ For multi-workspace setups, use **profiles** in `config.yaml`:
158159
```yaml
159160
profiles:
160161
app:
161-
target: '//app:app'
162-
platform: simulator
163-
buildMode: debug
162+
defaultTarget: '//app:app'
163+
defaultPlatform: simulator
164+
defaultBuildMode: debug
164165
mac:
165-
target: '//mac:mac'
166-
platform: macos
166+
defaultTarget: '//mac:mac'
167+
defaultPlatform: macos
167168
```
168169
169170
Then switch at runtime: `xcodebazelmcp set-defaults --profile app`
170171

172+
## Environment Variables
173+
174+
| Variable | Purpose |
175+
| ------------------------------ | ----------------------------------------------------------------------- |
176+
| `BAZEL_IOS_WORKSPACE` | Bazel workspace root (highest-precedence workspace source). |
177+
| `BAZEL_PATH` / `MCP_BAZEL_PATH`| Path to the Bazel/Bazelisk binary (defaults to `bazel` on `PATH`). |
178+
| `BAZEL_IOS_STARTUP_ARGS` | Bazel startup args prepended to every invocation (quote-aware). |
179+
| `BAZEL_IOS_MCP_MAX_OUTPUT` | Max captured command output in characters (default 200000). |
180+
| `BAZEL_IOS_SIMULATOR_CPU` | Override the iOS simulator CPU (`sim_arm64` / `x86_64`; default: host). |
181+
| `BAZEL_IOS_DISCOVER_SCOPE` | Default `discover` query scope for non-monorepo workspaces (e.g. `//...`). |
182+
| `BAZEL_IOS_COMMAND_LOG` | Path to the persistent NDJSON command log. |
183+
| `BAZEL_IOS_COMMAND_LOG_MAX_BYTES` | Rotate the command log past this size (default 5 MiB). |
184+
| `BAZEL_IOS_COMMAND_LOG_DISABLE`| Set to `1` to disable command logging. |
185+
| `IDB_PATH` | Path to the `idb` binary for UI automation (else discovered on `PATH`). |
186+
171187
## CLI Examples
172188

173189
```sh
@@ -207,31 +223,33 @@ xcodebazelmcp set-defaults --target //app:app --simulator-name "iPhone 16 Pro"
207223
xcodebazelmcp workflows
208224
```
209225

210-
## Workflow Categories (112 tools)
226+
## Workflow Categories (125 tools)
211227

212228
Workflows control which tools are advertised to MCP clients. Smart defaults enable the most common workflows; use `toggle-workflow` to customize.
213229

214-
| Category | Tools | Description |
215-
| ----------------- | ----- | ----------------------------------------------------- |
216-
| **build** | 2 | Build iOS targets for simulator or device |
217-
| **test** | 2 | Run iOS tests with optional coverage |
218-
| **simulator** | 10 | Manage simulator lifecycle and settings |
219-
| **app_lifecycle** | 5 | Install, launch, stop apps on simulator |
220-
| **capture** | 5 | Screenshot, video recording, log capture (simulator) |
221-
| **ui_automation** | 9 | Tap, swipe, type, drag, accessibility snapshot |
222-
| **deep_links** | 2 | Open URLs and send push notifications |
223-
| **device** | 13 | Physical device build, deploy, test, screenshot, logs |
224-
| **lldb** | 10 | LLDB debugger: breakpoints, variables, stepping |
225-
| **macos** | 13 | macOS build, run, test, discover |
226-
| **tvos** | 4 | tvOS build, run, test, discover |
227-
| **watchos** | 4 | watchOS build, run, test, discover |
228-
| **visionos** | 4 | visionOS build, run, test, discover |
229-
| **spm** | 7 | Swift Package Manager operations |
230-
| **project** | 6 | Target discovery, query, deps, rdeps |
231-
| **scaffold** | 2 | Generate new Bazel projects |
232-
| **session** | 7 | Workspace, defaults, profiles, health |
233-
| **daemon** | 3 | Background daemon management |
234-
| **update** | 2 | Self-update and version check |
230+
| Category | Tools | Description |
231+
| ----------------- | ----- | -------------------------------------------------------------------------------------- |
232+
| **build** | 2 | Build iOS targets for simulator or device |
233+
| **test** | 2 | Run iOS tests with optional coverage |
234+
| **simulator** | 12 | Manage simulator lifecycle, settings, media, and containers |
235+
| **app_lifecycle** | 6 | Install, launch, stop, uninstall apps on simulator |
236+
| **capture** | 5 | Screenshot, video recording, log capture (simulator) |
237+
| **agent_debug** | 4 | NDJSON agent debug logs (Cursor DEBUG MODE) |
238+
| **ui_automation** | 9 | Tap, swipe, type, drag, accessibility snapshot |
239+
| **deep_links** | 2 | Open URLs and send push notifications |
240+
| **device** | 16 | Physical device build, deploy, test, screenshot, logs, uninstall, list apps |
241+
| **lldb** | 10 | LLDB debugger: breakpoints, variables, stepping |
242+
| **macos** | 13 | macOS build, run, test, discover |
243+
| **tvos** | 4 | tvOS build, run, test, discover |
244+
| **watchos** | 4 | watchOS build, run, test, discover |
245+
| **visionos** | 4 | visionOS build, run, test, discover |
246+
| **spm** | 7 | Swift Package Manager operations |
247+
| **project** | 6 | Target discovery, query, deps, rdeps |
248+
| **scaffold** | 2 | Generate new Bazel projects |
249+
| **session** | 7 | Workspace, defaults, profiles, health |
250+
| **daemon** | 3 | Background daemon management |
251+
| **update** | 2 | Self-update and version check |
252+
| **xcode** | 3 | Apple-native Xcode MCP detection, DeviceHub, skills ([docs](docs/xcode-native-mcp.md)) |
235253

236254
## iOS 17+ Device Notes
237255

docs/xcode-native-mcp.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Xcode Native MCP Integration (Xcode 26.3+)
2+
3+
Apple ships a native Model Context Protocol (MCP) integration inside Xcode and
4+
LLDB. XcodeBazelMCP **detects and bridges** this tooling so an agent can use
5+
Xcode's own tools alongside the Bazel/simctl/devicectl flows — without replacing
6+
anything. Everything here is additive and degrades gracefully on older Xcode.
7+
8+
> Status: `mcpbridge` ships in **Xcode 26.3 (stable)**. `lldb-mcp` and
9+
> `DeviceHub.app` currently ship only in **Xcode-beta / Xcode 27**. The
10+
> integration is treated as **beta**: detection is always safe, and the existing
11+
> XcodeBazelMCP tools remain the supported path.
12+
13+
## What Apple provides
14+
15+
| Tool | Where | What it does |
16+
|------|-------|--------------|
17+
| `xcrun mcpbridge` | Xcode 26.3+ (`<dev>/usr/bin/mcpbridge`) | STDIO MCP bridge between an MCP client and a **running Xcode**'s tool service. Auto-selects the Xcode via `MCP_XCODE_PID` or `xcode-select`. |
18+
| `xcrun mcpbridge run-agent <agent>` | Xcode 26.3+ | Launch a coding agent (e.g. `claude`) pre-wired with Xcode's MCP tools, auth, and env. `--dry-run` prints the resolved command; `--no-xcode-tools` excludes them. |
19+
| `xcrun mcpbridge run-agent skills export` | Xcode 26.3+ | Export Xcode's globally available `SKILL.md` bundles (e.g. `device-interaction`, `swiftui-whats-new-27`). |
20+
| `lldb-mcp` | Xcode-beta / 27 (`<dev>/usr/bin/lldb-mcp`) | LLDB MCP server for debugging. |
21+
| `DeviceHub.app` | Xcode-beta / 27 (`<app>/Contents/Applications/DeviceHub.app`, bundle `com.apple.dt.Devices`) | GUI device manager — the successor to Xcode's Devices window. |
22+
23+
### Xcode's native device-interaction MCP tools
24+
25+
When connected through `mcpbridge` to a running Xcode 27, the agent gains
26+
Xcode's own device tools (surfaced by the `device-interaction` skill):
27+
28+
```
29+
DeviceInteractionStartSession → start a background device/sim session
30+
DeviceInteractionInstallAndRun → build + install + launch (commandLineArguments, environmentVariables)
31+
DeviceEventSynthesize → perform a touch/UI interaction and observe state (repeatable)
32+
DeviceInteractionEndSession → tear down (sessions are resource-heavy)
33+
```
34+
35+
These complement XcodeBazelMCP's `bazel_ios_*` UI-automation/device tools, which
36+
remain available for Bazel-built apps and pre-27 Xcode.
37+
38+
## What XcodeBazelMCP adds
39+
40+
Three opt-in tools (workflow `xcode`) and matching CLI commands:
41+
42+
| MCP tool | CLI | Purpose |
43+
|----------|-----|---------|
44+
| `bazel_xcode_native_mcp_status` | `xcodebazelmcp xcode-mcp-status` | Detect installs, `mcpbridge`/`lldb-mcp`/DeviceHub availability, running Xcode PIDs, and emit a ready-to-paste MCP client config. |
45+
| `bazel_xcode_open_device_hub` | `xcodebazelmcp devicehub` | Open DeviceHub.app for device interaction (clear guidance when not installed). |
46+
| `bazel_xcode_export_skills` | `xcodebazelmcp xcode-export-skills [--output-dir <dir>] [--replace-existing]` | Export Xcode's agent skill bundles (needs a running Xcode). |
47+
48+
Enable the workflow for MCP clients (it is **off by default**, like other
49+
optional categories):
50+
51+
```sh
52+
xcodebazelmcp toggle-workflow xcode on
53+
```
54+
55+
The CLI commands work regardless of workflow filtering.
56+
57+
## Wiring Xcode's native tools into your MCP client
58+
59+
`xcode-mcp-status` prints a drop-in snippet. Add it next to XcodeBazelMCP so the
60+
agent sees both tool sets (Bazel tools from XcodeBazelMCP, Xcode IDE/device tools
61+
from `mcpbridge`):
62+
63+
```json
64+
{
65+
"mcpServers": {
66+
"xcodebazelmcp": { "command": "npx", "args": ["xcodebazelmcp", "mcp"] },
67+
"xcode-native": { "command": "xcrun", "args": ["mcpbridge"] }
68+
}
69+
}
70+
```
71+
72+
- `mcpbridge` requires a **running Xcode**. With multiple Xcodes installed it
73+
uses `xcode-select`; pin one with `MCP_XCODE_PID=<pid>` or
74+
`env: { "DEVELOPER_DIR": "/Applications/Xcode-beta.app/Contents/Developer" }`.
75+
- To target the beta toolchain for `lldb-mcp`/DeviceHub, set `DEVELOPER_DIR`
76+
before invoking, or select Xcode-beta via `xcode-select`.
77+
78+
## Backwards compatibility
79+
80+
- Detection never throws and never shells out destructively.
81+
- On Xcode < 26.3 (no `mcpbridge`), `xcode-mcp-status` reports it's unavailable
82+
and the Bazel/simctl/devicectl tools continue to be the supported path.
83+
- `bazel_xcode_open_device_hub` returns actionable guidance (use
84+
`bazel_ios_list_devices` / `bazel_ios_device_info`) when DeviceHub is absent.
85+
86+
## Quick check
87+
88+
```sh
89+
xcodebazelmcp xcode-mcp-status
90+
# → lists Xcode.app / Xcode-beta.app with mcpbridge/lldb-mcp/DeviceHub flags,
91+
# running Xcode PIDs, and the client-config snippet.
92+
```

scripts/smoke.mjs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { existsSync, readFileSync } from 'node:fs';
1+
import { execFileSync } from 'node:child_process';
2+
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
3+
import { tmpdir } from 'node:os';
24
import { join, resolve } from 'node:path';
35

46
const root = resolve(new URL('..', import.meta.url).pathname);
@@ -31,4 +33,28 @@ for (const toolName of ['bazel_ios_build', 'bazel_ios_test', 'bazel_ios_discover
3133
}
3234
}
3335

36+
// Runtime smoke: boot the CLI (no Bazel/Xcode needed) and exercise scaffolding.
37+
const cli = join(root, 'src/cli.ts');
38+
const runCli = (args) =>
39+
execFileSync('npx', ['tsx', cli, ...args], { cwd: root, encoding: 'utf8', timeout: 60_000 });
40+
41+
const toolsOut = runCli(['tools']);
42+
const toolCount = toolsOut.split('\n').filter((l) => /^[a-z_]+$/.test(l)).length;
43+
if (toolCount !== 125) {
44+
throw new Error(`Expected 125 tools, CLI listed ${toolCount}`);
45+
}
46+
for (const t of ['bazel_ios_uninstall_app', 'bazel_ios_device_uninstall_app', 'bazel_ios_device_list_apps']) {
47+
if (!toolsOut.includes(t)) throw new Error(`tools output missing ${t}`);
48+
}
49+
50+
const scaffoldDir = mkdtempSync(join(tmpdir(), 'xbmcp-smoke-'));
51+
try {
52+
runCli(['new', 'ios_app', 'SmokeApp', '-o', scaffoldDir]);
53+
for (const f of ['MODULE.bazel', '.bazelrc', 'SmokeApp/BUILD.bazel', 'SmokeApp/Info.plist']) {
54+
if (!existsSync(join(scaffoldDir, f))) throw new Error(`scaffold missing ${f}`);
55+
}
56+
} finally {
57+
rmSync(scaffoldDir, { recursive: true, force: true });
58+
}
59+
3460
console.log('smoke ok');

src/cli.test.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ describe('CLI help', () => {
1717
expect(out).toContain('Usage:');
1818
expect(out).toContain('xcodebazelmcp mcp');
1919
expect(out).toContain('Build & Run:');
20+
expect(out).toContain('--launch-env KEY=VAL');
2021
expect(out).toContain('Query & Inspect:');
2122
expect(out).toContain('Config & Defaults:');
2223
expect(out).toContain('Simulator:');
@@ -40,13 +41,34 @@ describe('CLI help', () => {
4041
});
4142
});
4243

44+
describe('CLI error handling', () => {
45+
it('prints a clean error (no stack trace) for an invalid label', () => {
46+
let stdout = '';
47+
let stderr = '';
48+
let exitCode = 0;
49+
try {
50+
stdout = run(['deps', 'not a label']);
51+
} catch (e) {
52+
const err = e as { status?: number; stdout?: string; stderr?: string };
53+
exitCode = err.status ?? 1;
54+
stdout = err.stdout ?? '';
55+
stderr = err.stderr ?? '';
56+
}
57+
expect(exitCode).toBe(1);
58+
const combined = stdout + stderr;
59+
expect(combined).toContain('Error: target must be a Bazel label');
60+
expect(combined).not.toContain('at requireLabel');
61+
expect(combined).not.toContain('.js:');
62+
});
63+
});
64+
4365
describe('CLI tools', () => {
44-
it('lists all 117 tools', () => {
66+
it('lists all 125 tools', () => {
4567
const out = run(['tools']);
4668
const toolLines = out
4769
.split('\n')
4870
.filter((line) => line.match(/^[a-z_]+$/));
49-
expect(toolLines.length).toBe(117);
71+
expect(toolLines.length).toBe(125);
5072
expect(out).toContain('bazel_ios_build');
5173
expect(out).toContain('bazel_macos_build');
5274
expect(out).toContain('bazel_tvos_build');

0 commit comments

Comments
 (0)