Driving Xcode from Claude Code
Working model: Claude Code is the editor; Xcode is an attached
build/run/debug/docs service. The user is not editing in Xcode — it's open
so its services are reachable. Never assume the Xcode UI state (current file,
selection) means anything unless asked about it, and remember some calls
mutate state the human also sees (run destination, debugger, simulator).
Per-tool verdicts and nuances: references/tool-reference.md.
Session bootstrap (once per session)
XcodeListWindows → parse tabIdentifier from the message text; match
on workspacePath if several windows. No workspace open → everything else
is unusable; ask the user or open -a Xcode-beta <path>.xcodeproj.
- Only if you'll build/run: confirm the destination via the
activeDestinationDisplayTitle field of XcodeSwitchScheme/…Destination
responses — do NOT call XcodeListRunDestinations (15KB) unless you
actually need to change OS/device class.
tabIdentifier is Xcode-side state: survives agent restarts, dies with
Xcode. Tools enumerate at agent-session start — if Xcode wasn't running
then, the tools are absent until reconnect/restart.
Division of labor — native tools vs xcode-tools vs CLI
The project type matters: in SwiftPM packages (Package.swift),
membership IS the folder layout — native file tools do everything, and the
Xcode file tools add nothing. In .xcodeproj projects, membership lives
in the pbxproj — file lifecycle must go through Xcode tools or the build
won't see your changes.
| Job |
Use |
Don't use |
Why |
| Read file contents |
native Read |
XcodeRead |
same bytes without the MCP hop, JSON-escaped payload, or tabIdentifier ceremony |
| Search/list source |
native Grep/Glob |
XcodeGrep/Glob/LS |
faster; content is content |
| Edit existing file |
native Edit |
XcodeUpdate |
Edit errors on ambiguous match; XcodeUpdate silently edits the FIRST occurrence |
| Create file (.xcodeproj) |
XcodeWrite |
native Write |
registers it in project/target; native Write orphans it |
| Delete/rename/move (.xcodeproj) |
XcodeRM / XcodeMV |
native rm/mv |
keeps pbxproj references consistent; RM trashes (recoverable) |
| Any file op (SwiftPM pkg) |
native tools |
Xcode file tools |
folder = membership |
| Group structure / target membership questions |
XcodeLS, GetTargetBuildSettings |
guessing from disk |
groups ≠ folders |
| "What is the user looking at?" |
XcodeGetCurrentFile |
— |
only source of editor focus; {"isEditable":false} = nothing focused |
| One-file compile check |
XcodeRefreshCodeIssuesInFile |
building |
no build artifacts touched; sees more than the compiler emits (but ~7–10s — a warm incremental BuildProject can be faster) |
| Full build |
BuildProject |
shelling to xcodebuild |
quiet by design (summary + log file); CLI only for clean builds / flag overrides / CI parity |
| Run tests (.xcodeproj / simulator) |
RunSomeTests / RunAllTests |
xcodebuild test |
structured counts + .xcresult, self-building |
| Run tests (SwiftPM) |
swift test CLI |
MCP test tools |
no scheme/simulator needed; CLI is leaner |
| Launch app + debug |
RunProject(attachDebugger:true) → InvokeDebuggerCommand |
— |
the only lldb path; CLI lldb would fight Xcode |
| App stdout/print + OSLog of an Xcode-launched run |
GetConsoleOutput |
log show |
print/stdout never reaches unified logging; session-scoped |
| Logs of a process YOU launched via CLI |
log show/log stream |
GetConsoleOutput |
it only sees Xcode launch sessions |
| Visual check of a SwiftUI view |
RenderPreview |
building+screenshot rituals |
renders #Preview to PNG, ~25–30s, no simulator interaction |
| Try an idea in project context |
RunCodeSnippet |
scratch files |
sees the file's types; stdout-only output |
| Build settings / Info.plist / entitlements |
the MCP setting tools |
editing pbxproj/plist by hand |
validated, reference-safe (entitlements are checked against a real database) |
| String Catalog work |
StringCatalog* tools |
native Edit of .xcstrings |
tools maintain the translation state machine + extraction sync |
| Apple docs lookup |
see the apple-docs skill's routing section |
— |
DocumentationSearch is discovery, NOT an existence check |
| iOS UI driving (tap/type/screenshot) |
DeviceInteraction* suite |
— |
requires iOS Simulator 27.0+ runtime; iOS-only — explicitly refuses Mac |
What only this server gives you
XcodeRefreshCodeIssuesInFile — live sourcekit diagnostics with no
build, seeing more than the compiler emits. Not instant (~7–10s
observed): prefer it when a build is undesirable; on warm small
projects an incremental BuildProject is both faster and authoritative.
InvokeDebuggerCommand — a real lldb into the running app, sharing
Xcode's debug-console session. Etiquette: the human sees your commands;
process interrupt before expressions, continue promptly, remove any
breakpoints you set.
RenderPreview — compile-and-snapshot any #Preview to a PNG.
- Membership-aware file ops for .xcodeproj projects.
DeviceInteractionSynthesize — taps/swipes/typing + screenshot + UI
hierarchy + app stderr per call, with command chaining (verified live;
needs an iOS 27+ simulator runtime — iOS only, Mac refused).
- Crash/field-performance services — Apple's field data + triage docs,
for apps connected in Organizer (pass
bundle_id explicitly; the
auto-resolution claim is false in practice).
Verification ladder (cheapest first — but read the timings)
BuildProject — authoritative compile+link; self-reports elapsedTime
(0.1s null build, ~4s small warm compile). On warm projects this is
usually the cheapest real check.
XcodeRefreshCodeIssuesInFile — per-file, no build artifacts, catches
hallucinated APIs and more diagnostics than the compiler (~7–10s).
Misses cross-file/link breaks.
RunCodeSnippet — behavior probe in project context (~15–25s).
RunSomeTests → RunAllTests — highest signal; ~14s with a warm
simulator, 45s+ cold. Once the sim is booted, tests are cheap — use them.
Token cost & noise control
Most responses are pre-quieted summaries; the xcodebuild firehose goes to
artifact files (/var/folders/…/T/ActionArtifacts/default/<Tool>/…) — grep
those, never read linearly. The exceptions to watch:
| Offender |
Cost |
Mitigation |
| GetTargetBuildSettings |
~26KB inline, no filter |
fetch via subagent, or accept once and extract what you need |
| DocumentationSearch |
30KB/query (12KB with frameworks filter) |
filter + precise queries; it's vocabulary discovery, not lookup |
| XcodeListRunDestinations |
~15KB |
avoid; read active destination from switch-tool responses |
| GetConsoleOutput |
500-line default tail |
pattern + oslogSeverity + small tailLimit |
Gotcha: GetBuildLog and XcodeListNavigatorIssues default to
severity:"error" — a warnings-only build looks empty until you pass
severity:"warning".
Failure modes & repair
| Symptom |
Cause → fix |
Server dead at startup: MCP_XCODE_PID … no running Xcode processes found |
Xcode wasn't running → open Xcode, reconnect/restart session |
| Tools entirely absent |
same; enumeration happens once at session start |
No workspace windows found |
no project open in Xcode |
| Wrong Xcode answering |
set MCP_XCODE_PID=<pid> in server env |
Device tools: Supported: iOS [Simulator] 27.0+ |
install the iOS 27 simulator runtime; Mac is never supported |
Hard MCP error {"type":"error","data":…} vs result with success:false |
two error envelopes — check both |
Setup (one-time):
claude mcp add --scope user xcode-tools \
--env DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer \
-- /Applications/Xcode-beta.app/Contents/Developer/usr/bin/mcpbridge
1---2name: xcode-tools3description: Operating guide for Xcode 27's bundled MCP server (mcpbridge): when to use each mcp__xcode-tools__* tool versus the agent's native tools or the CLI, connection setup and repair, output-noise control, and the tools that are genuinely unique (live per-file diagnostics, the shared lldb session, preview rendering, project-membership file operations, device driving). Use whenever xcode-tools MCP tools are available and an Xcode project is involved — including building, running, testing, debugging, rendering previews, or driving the simulator — before reaching for any mcp__xcode-tools__* tool, or when those tools are missing or erroring.4---56# Driving Xcode from Claude Code78Working model: **Claude Code is the editor; Xcode is an attached9build/run/debug/docs service.** The user is not editing in Xcode — it's open10so its services are reachable. Never assume the Xcode UI state (current file,11selection) means anything unless asked about it, and remember some calls12mutate state the *human* also sees (run destination, debugger, simulator).1314Per-tool verdicts and nuances: `references/tool-reference.md`.1516## Session bootstrap (once per session)17181. `XcodeListWindows` → parse `tabIdentifier` from the `message` text; match19 on `workspacePath` if several windows. No workspace open → everything else20 is unusable; ask the user or `open -a Xcode-beta <path>.xcodeproj`.212. Only if you'll build/run: confirm the destination via the22 `activeDestinationDisplayTitle` field of `XcodeSwitchScheme`/`…Destination`23 responses — do NOT call `XcodeListRunDestinations` (15KB) unless you24 actually need to change OS/device class.2526`tabIdentifier` is Xcode-side state: survives agent restarts, dies with27Xcode. Tools enumerate at agent-session start — if Xcode wasn't running28then, the tools are absent until reconnect/restart.2930## Division of labor — native tools vs xcode-tools vs CLI3132The project type matters: in **SwiftPM packages** (Package.swift),33membership IS the folder layout — native file tools do everything, and the34Xcode file tools add nothing. In **.xcodeproj projects**, membership lives35in the pbxproj — file *lifecycle* must go through Xcode tools or the build36won't see your changes.3738| Job | Use | Don't use | Why |39|---|---|---|---|40| Read file contents | native Read | XcodeRead | same bytes without the MCP hop, JSON-escaped payload, or tabIdentifier ceremony |41| Search/list source | native Grep/Glob | XcodeGrep/Glob/LS | faster; content is content |42| Edit existing file | native Edit | XcodeUpdate | Edit errors on ambiguous match; XcodeUpdate silently edits the FIRST occurrence |43| Create file (.xcodeproj) | XcodeWrite | native Write | registers it in project/target; native Write orphans it |44| Delete/rename/move (.xcodeproj) | XcodeRM / XcodeMV | native rm/mv | keeps pbxproj references consistent; RM trashes (recoverable) |45| Any file op (SwiftPM pkg) | native tools | Xcode file tools | folder = membership |46| Group structure / target membership questions | XcodeLS, GetTargetBuildSettings | guessing from disk | groups ≠ folders |47| "What is the user looking at?" | XcodeGetCurrentFile | — | only source of editor focus; `{"isEditable":false}` = nothing focused |48| One-file compile check | XcodeRefreshCodeIssuesInFile | building | no build artifacts touched; sees more than the compiler emits (but ~7–10s — a warm incremental BuildProject can be faster) |49| Full build | BuildProject | shelling to xcodebuild | quiet by design (summary + log file); CLI only for clean builds / flag overrides / CI parity |50| Run tests (.xcodeproj / simulator) | RunSomeTests / RunAllTests | xcodebuild test | structured counts + .xcresult, self-building |51| Run tests (SwiftPM) | `swift test` CLI | MCP test tools | no scheme/simulator needed; CLI is leaner |52| Launch app + debug | RunProject(attachDebugger:true) → InvokeDebuggerCommand | — | the only lldb path; CLI lldb would fight Xcode |53| App stdout/print + OSLog of an Xcode-launched run | GetConsoleOutput | `log show` | print/stdout never reaches unified logging; session-scoped |54| Logs of a process YOU launched via CLI | `log show`/`log stream` | GetConsoleOutput | it only sees Xcode launch sessions |55| Visual check of a SwiftUI view | RenderPreview | building+screenshot rituals | renders #Preview to PNG, ~25–30s, no simulator interaction |56| Try an idea in project context | RunCodeSnippet | scratch files | sees the file's types; stdout-only output |57| Build settings / Info.plist / entitlements | the MCP setting tools | editing pbxproj/plist by hand | validated, reference-safe (entitlements are checked against a real database) |58| String Catalog work | StringCatalog* tools | native Edit of .xcstrings | tools maintain the translation state machine + extraction sync |59| Apple docs lookup | see the `apple-docs` skill's routing section | — | DocumentationSearch is discovery, NOT an existence check |60| iOS UI driving (tap/type/screenshot) | DeviceInteraction* suite | — | requires iOS Simulator 27.0+ runtime; iOS-only — explicitly refuses Mac |6162## What only this server gives you63641. **`XcodeRefreshCodeIssuesInFile`** — live sourcekit diagnostics with no65 build, seeing more than the compiler emits. Not instant (~7–10s66 observed): prefer it when a build is undesirable; on warm small67 projects an incremental BuildProject is both faster and authoritative.682. **`InvokeDebuggerCommand`** — a real lldb into the running app, sharing69 Xcode's debug-console session. Etiquette: the human sees your commands;70 `process interrupt` before expressions, `continue` promptly, remove any71 breakpoints you set.723. **`RenderPreview`** — compile-and-snapshot any `#Preview` to a PNG.734. **Membership-aware file ops** for .xcodeproj projects.745. **`DeviceInteractionSynthesize`** — taps/swipes/typing + screenshot + UI75 hierarchy + app stderr per call, with command chaining (verified live;76 needs an iOS 27+ simulator runtime — iOS only, Mac refused).776. **Crash/field-performance services** — Apple's field data + triage docs,78 for apps connected in Organizer (pass `bundle_id` explicitly; the79 auto-resolution claim is false in practice).8081## Verification ladder (cheapest first — but read the timings)82831. `BuildProject` — authoritative compile+link; self-reports `elapsedTime`84 (0.1s null build, ~4s small warm compile). On warm projects this is85 usually the cheapest real check.862. `XcodeRefreshCodeIssuesInFile` — per-file, no build artifacts, catches87 hallucinated APIs and more diagnostics than the compiler (~7–10s).88 Misses cross-file/link breaks.893. `RunCodeSnippet` — behavior probe in project context (~15–25s).904. `RunSomeTests` → `RunAllTests` — highest signal; ~14s with a warm91 simulator, 45s+ cold. Once the sim is booted, tests are cheap — use them.9293## Token cost & noise control9495Most responses are pre-quieted summaries; the xcodebuild firehose goes to96artifact files (`/var/folders/…/T/ActionArtifacts/default/<Tool>/…`) — grep97those, never read linearly. The exceptions to watch:9899| Offender | Cost | Mitigation |100|---|---|---|101| GetTargetBuildSettings | ~26KB inline, no filter | fetch via subagent, or accept once and extract what you need |102| DocumentationSearch | ~30KB/query (~12KB with `frameworks` filter) | filter + precise queries; it's vocabulary discovery, not lookup |103| XcodeListRunDestinations | ~15KB | avoid; read active destination from switch-tool responses |104| GetConsoleOutput | 500-line default tail | `pattern` + `oslogSeverity` + small `tailLimit` |105106Gotcha: `GetBuildLog` and `XcodeListNavigatorIssues` default to107`severity:"error"` — a warnings-only build looks empty until you pass108`severity:"warning"`.109110## Failure modes & repair111112| Symptom | Cause → fix |113|---|---|114| Server dead at startup: `MCP_XCODE_PID … no running Xcode processes found` | Xcode wasn't running → open Xcode, reconnect/restart session |115| Tools entirely absent | same; enumeration happens once at session start |116| `No workspace windows found` | no project open in Xcode |117| Wrong Xcode answering | set `MCP_XCODE_PID=<pid>` in server env |118| Device tools: `Supported: iOS [Simulator] 27.0+` | install the iOS 27 simulator runtime; Mac is never supported |119| Hard MCP error `{"type":"error","data":…}` vs result with `success:false` | two error envelopes — check both |120121Setup (one-time):122123```bash124claude mcp add --scope user xcode-tools \125 --env DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer \126 -- /Applications/Xcode-beta.app/Contents/Developer/usr/bin/mcpbridge127```