Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ jobs:
- name: Build
run: npm run build

- name: Smoke (CLI boot, tool inventory, scaffold)
run: npm run smoke

tag:
name: Bump version & tag
needs: [ci, build]
Expand Down
96 changes: 57 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,21 @@

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

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

</div>

<br>
Expand Down Expand Up @@ -147,7 +148,7 @@ npm run build

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

1. **MCP tool at runtime** — `bazel_ios_set_workspace` (or CLI `set-defaults --target //app:app`)
1. **MCP tool at runtime** — `bazel_ios_set_workspace` (sets the workspace for later tool calls)
2. **CLI flag** — `xcodebazelmcp mcp --workspace /path/to/workspace`
3. **Environment variable** — `BAZEL_IOS_WORKSPACE=/path/to/workspace`
4. **Config file** — `.xcodebazelmcp/config.yaml` in the workspace root (supports profiles)
Expand All @@ -158,16 +159,31 @@ For multi-workspace setups, use **profiles** in `config.yaml`:
```yaml
profiles:
app:
target: '//app:app'
platform: simulator
buildMode: debug
defaultTarget: '//app:app'
defaultPlatform: simulator
defaultBuildMode: debug
mac:
target: '//mac:mac'
platform: macos
defaultTarget: '//mac:mac'
defaultPlatform: macos
```

Then switch at runtime: `xcodebazelmcp set-defaults --profile app`

## Environment Variables

| 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`). |

## CLI Examples

```sh
Expand Down Expand Up @@ -207,31 +223,33 @@ xcodebazelmcp set-defaults --target //app:app --simulator-name "iPhone 16 Pro"
xcodebazelmcp workflows
```

## Workflow Categories (112 tools)
## Workflow Categories (125 tools)

Workflows 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** | 10 | Manage simulator lifecycle and settings |
| **app_lifecycle** | 5 | Install, launch, stop apps on simulator |
| **capture** | 5 | Screenshot, video recording, log capture (simulator) |
| **ui_automation** | 9 | Tap, swipe, type, drag, accessibility snapshot |
| **deep_links** | 2 | Open URLs and send push notifications |
| **device** | 13 | Physical device build, deploy, test, screenshot, logs |
| **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 |
| 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](docs/xcode-native-mcp.md)) |

## iOS 17+ Device Notes

Expand Down
92 changes: 92 additions & 0 deletions docs/xcode-native-mcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Xcode Native MCP Integration (Xcode 26.3+)

Apple ships a native Model Context Protocol (MCP) integration inside Xcode and
LLDB. XcodeBazelMCP **detects and bridges** this tooling so an agent can use
Xcode's own tools alongside the Bazel/simctl/devicectl flows — without replacing
anything. Everything here is additive and degrades gracefully on older Xcode.

> Status: `mcpbridge` ships in **Xcode 26.3 (stable)**. `lldb-mcp` and
> `DeviceHub.app` currently ship only in **Xcode-beta / Xcode 27**. The
> integration is treated as **beta**: detection is always safe, and the existing
> XcodeBazelMCP tools remain the supported path.

## What Apple provides

| Tool | Where | What it does |
|------|-------|--------------|
| `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`. |
| `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. |
| `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`). |
| `lldb-mcp` | Xcode-beta / 27 (`<dev>/usr/bin/lldb-mcp`) | LLDB MCP server for debugging. |
| `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. |

### Xcode's native device-interaction MCP tools

When connected through `mcpbridge` to a running Xcode 27, the agent gains
Xcode's own device tools (surfaced by the `device-interaction` skill):

```
DeviceInteractionStartSession → start a background device/sim session
DeviceInteractionInstallAndRun → build + install + launch (commandLineArguments, environmentVariables)
DeviceEventSynthesize → perform a touch/UI interaction and observe state (repeatable)
DeviceInteractionEndSession → tear down (sessions are resource-heavy)
```

These complement XcodeBazelMCP's `bazel_ios_*` UI-automation/device tools, which
remain available for Bazel-built apps and pre-27 Xcode.

## What XcodeBazelMCP adds

Three opt-in tools (workflow `xcode`) and matching CLI commands:

| MCP tool | CLI | Purpose |
|----------|-----|---------|
| `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. |
| `bazel_xcode_open_device_hub` | `xcodebazelmcp devicehub` | Open DeviceHub.app for device interaction (clear guidance when not installed). |
| `bazel_xcode_export_skills` | `xcodebazelmcp xcode-export-skills [--output-dir <dir>] [--replace-existing]` | Export Xcode's agent skill bundles (needs a running Xcode). |

Enable the workflow for MCP clients (it is **off by default**, like other
optional categories):

```sh
xcodebazelmcp toggle-workflow xcode on
```

The CLI commands work regardless of workflow filtering.

## Wiring Xcode's native tools into your MCP client

`xcode-mcp-status` prints a drop-in snippet. Add it next to XcodeBazelMCP so the
agent sees both tool sets (Bazel tools from XcodeBazelMCP, Xcode IDE/device tools
from `mcpbridge`):

```json
{
"mcpServers": {
"xcodebazelmcp": { "command": "npx", "args": ["xcodebazelmcp", "mcp"] },
"xcode-native": { "command": "xcrun", "args": ["mcpbridge"] }
}
}
```

- `mcpbridge` requires a **running Xcode**. With multiple Xcodes installed it
uses `xcode-select`; pin one with `MCP_XCODE_PID=<pid>` or
`env: { "DEVELOPER_DIR": "/Applications/Xcode-beta.app/Contents/Developer" }`.
- To target the beta toolchain for `lldb-mcp`/DeviceHub, set `DEVELOPER_DIR`
before invoking, or select Xcode-beta via `xcode-select`.

## Backwards compatibility

- Detection never throws and never shells out destructively.
- On Xcode < 26.3 (no `mcpbridge`), `xcode-mcp-status` reports it's unavailable
and the Bazel/simctl/devicectl tools continue to be the supported path.
- `bazel_xcode_open_device_hub` returns actionable guidance (use
`bazel_ios_list_devices` / `bazel_ios_device_info`) when DeviceHub is absent.

## Quick check

```sh
xcodebazelmcp xcode-mcp-status
# → lists Xcode.app / Xcode-beta.app with mcpbridge/lldb-mcp/DeviceHub flags,
# running Xcode PIDs, and the client-config snippet.
```
28 changes: 27 additions & 1 deletion scripts/smoke.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { existsSync, readFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';

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

// Runtime smoke: boot the CLI (no Bazel/Xcode needed) and exercise scaffolding.
const cli = join(root, 'src/cli.ts');
const runCli = (args) =>
execFileSync('npx', ['tsx', cli, ...args], { cwd: root, encoding: 'utf8', timeout: 60_000 });

const toolsOut = runCli(['tools']);
const toolCount = toolsOut.split('\n').filter((l) => /^[a-z_]+$/.test(l)).length;
if (toolCount !== 125) {
throw new Error(`Expected 125 tools, CLI listed ${toolCount}`);
}
for (const t of ['bazel_ios_uninstall_app', 'bazel_ios_device_uninstall_app', 'bazel_ios_device_list_apps']) {
if (!toolsOut.includes(t)) throw new Error(`tools output missing ${t}`);
}

const scaffoldDir = mkdtempSync(join(tmpdir(), 'xbmcp-smoke-'));
try {
runCli(['new', 'ios_app', 'SmokeApp', '-o', scaffoldDir]);
for (const f of ['MODULE.bazel', '.bazelrc', 'SmokeApp/BUILD.bazel', 'SmokeApp/Info.plist']) {
if (!existsSync(join(scaffoldDir, f))) throw new Error(`scaffold missing ${f}`);
}
} finally {
rmSync(scaffoldDir, { recursive: true, force: true });
}

console.log('smoke ok');
26 changes: 24 additions & 2 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ describe('CLI help', () => {
expect(out).toContain('Usage:');
expect(out).toContain('xcodebazelmcp mcp');
expect(out).toContain('Build & Run:');
expect(out).toContain('--launch-env KEY=VAL');
expect(out).toContain('Query & Inspect:');
expect(out).toContain('Config & Defaults:');
expect(out).toContain('Simulator:');
Expand All @@ -40,13 +41,34 @@ describe('CLI help', () => {
});
});

describe('CLI error handling', () => {
it('prints a clean error (no stack trace) for an invalid label', () => {
let stdout = '';
let stderr = '';
let exitCode = 0;
try {
stdout = run(['deps', 'not a label']);
} catch (e) {
const err = e as { status?: number; stdout?: string; stderr?: string };
exitCode = err.status ?? 1;
stdout = err.stdout ?? '';
stderr = err.stderr ?? '';
}
expect(exitCode).toBe(1);
const combined = stdout + stderr;
expect(combined).toContain('Error: target must be a Bazel label');
expect(combined).not.toContain('at requireLabel');
expect(combined).not.toContain('.js:');
});
});

describe('CLI tools', () => {
it('lists all 117 tools', () => {
it('lists all 125 tools', () => {
const out = run(['tools']);
const toolLines = out
.split('\n')
.filter((line) => line.match(/^[a-z_]+$/));
expect(toolLines.length).toBe(117);
expect(toolLines.length).toBe(125);
expect(out).toContain('bazel_ios_build');
expect(out).toContain('bazel_macos_build');
expect(out).toContain('bazel_tvos_build');
Expand Down
Loading
Loading