This document describes how the simulator debugging tools are wired, how sessions are managed, and how external tools (simctl, Simulator, LLDB, xcodebuild) are invoked.
- Tools:
src/mcp/tools/debugging/* - Debugger subsystem:
src/utils/debugger/* - Execution and tool wiring:
src/utils/typed-tool-factory.ts,src/utils/execution/* - External invocation:
xcrun simctl,xcrun lldb,xcodebuild
- Workflow discovery is automatic:
src/core/plugin-registry.tsloads debugging tools via the generated workflow loaders (src/core/generated-plugins.ts). - Tool handlers are created with the typed tool factory:
createTypedToolWithContextfor standard tools (Zod validation + dependency injection).createSessionAwareToolWithContextfor session-aware tools (merges session defaults and validates requirements).
- Debugging tools inject a
DebuggerToolContextthat provides:executor: aCommandExecutorused for simctl and other command execution.debugger: a sharedDebuggerManagerinstance.
- Session defaults live in
src/utils/session-store.tsand are merged with user args by the session-aware tool factory. debug_attach_simis session-aware; it can omitsimulatorId/simulatorNamein the public schema and rely on session defaults.- The
XCODEBUILDMCP_DISABLE_SESSION_DEFAULTSenv flag exposes legacy schemas that include all parameters (no session default hiding).
DebuggerManager owns lifecycle, state, and backend routing:
Backend selection happens inside DebuggerManager.createSession:
- Selection order: explicit
backendargument ->XCODEBUILDMCP_DEBUGGER_BACKEND-> defaultlldb-cli. - Env values:
lldb-cli/lldb->lldb-cli,dap->dap, anything else throws. - Backend factory:
defaultBackendFactorymapslldb-clitocreateLldbCliBackendanddaptocreateDapBackend. A custom factory can be injected for tests or extensions.
debug_attach_simresolves simulator UUID and PID, then callsDebuggerManager.createSession.DebuggerManagercreates a backend (defaultlldb-cli), attaches to the process, and stores session metadata (id, simulatorId, pid, timestamps).- Debugging tools (
debug_lldb_command,debug_stack,debug_variables,debug_breakpoint_add/remove) look up the session (explicit id or current) and route commands to the backend. debug_detachcallsDebuggerManager.detachSessionto detach and dispose the backend.
Session lifecycle flow (text):
- Client calls
debug_attach_sim. debug_attach_simresolves simulator UUID and PID, then callsDebuggerManager.createSession.DebuggerManager.createSessionresolves backend kind (explicit/env/default), instantiates the backend, and callsbackend.attach.- Command tools (
debug_lldb_command,debug_stack,debug_variables) callDebuggerManager.runCommand/getStack/getVariables, which route to the backend. debug_detachcallsDebuggerManager.detachSession, which invokesbackend.detachandbackend.dispose.
LldbCliBackend.runCommand() flow (text):
- Enqueue the command to serialize LLDB access.
- Await backend readiness (
initializecompleted). - Write the command to the interactive process.
- Write
script print("__XCODEBUILDMCP_DONE__")to emit the sentinel marker. - Buffer stdout/stderr until the sentinel is detected.
- Trim the buffer to the next prompt, sanitize output, and return the result.
Sequence diagrams (Mermaid)
sequenceDiagram
participant U as User/Client
participant A as debug_attach_sim
participant M as DebuggerManager
participant F as backendFactory
participant B as DebuggerBackend (lldb-cli|dap)
participant L as LldbCliBackend
participant P as InteractiveProcess (xcrun lldb)
U->>A: debug_attach_sim(simulator*, bundleId|pid)
A->>A: determineSimulatorUuid(...)
A->>A: resolveSimulatorAppPid(...) (if bundleId)
A->>M: createSession({simulatorId, pid, waitFor})
M->>M: resolveBackendKind(explicit/env/default)
M->>F: create backend(kind)
F-->>M: backend instance
M->>B: attach({pid, simulatorId, waitFor})
alt kind == lldb-cli
B-->>L: (is LldbCliBackend)
L->>P: spawn xcrun lldb + initialize prompt/sentinel
else kind == dap
B-->>M: throws DAP_ERROR_MESSAGE
end
M-->>A: DebugSessionInfo {id, backend, ...}
A->>M: setCurrentSession(id) (optional)
U->>M: runCommand(id?, "thread backtrace")
M->>B: runCommand(...)
U->>M: detachSession(id?)
M->>B: detach()
M->>B: dispose()
sequenceDiagram
participant T as debug_lldb_command
participant M as DebuggerManager
participant L as LldbCliBackend
participant P as InteractiveProcess
participant S as stdout/stderr buffer
T->>M: runCommand(sessionId?, command, {timeoutMs?})
M->>L: runCommand(command)
L->>L: enqueue(work)
L->>L: await ready (initialize())
L->>P: write(command + "\n")
L->>P: write('script print("__XCODEBUILDMCP_DONE__")\n')
P-->>S: stdout/stderr chunks
S-->>L: handleData() appends to buffer
L->>L: checkPending() finds sentinel
L->>L: slice output up to sentinel
L->>L: trim buffer to next prompt (if present)
L->>L: sanitizeOutput() + trimEnd()
L-->>M: output string
M-->>T: output string
- Backend implementation:
src/utils/debugger/backends/lldb-cli-backend.ts. - Uses
InteractiveSpawnerfromsrc/utils/execution/interactive-process.tsto keep a single long-livedxcrun lldbprocess alive. - Keeps LLDB state (breakpoints, selected frames, target) across tool calls without reattaching.
- The backend spawns
xcrun lldb --no-lldbinit -o "settings set prompt <prompt>". InteractiveProcess.write()is used to send commands; stdout and stderr are merged into a single parse buffer.InteractiveProcess.dispose()closes stdin, removes listeners, and kills the process.
The backend uses a prompt + sentinel protocol to detect command completion reliably:
LLDB_PROMPT = "XCODEBUILDMCP_LLDB> "COMMAND_SENTINEL = "__XCODEBUILDMCP_DONE__"
Definitions:
- Prompt: the LLDB REPL prompt string that indicates LLDB is ready to accept the next command.
- Sentinel: a unique marker explicitly printed after each command to mark the end of that command's output.
Protocol flow:
- Startup: write
script print("__XCODEBUILDMCP_DONE__")to prime the prompt parser. - For each command:
- Write the command.
- Write
script print("__XCODEBUILDMCP_DONE__"). - Read until the sentinel is observed, then trim up to the next prompt.
The sentinel marks command completion, while the prompt indicates the REPL is ready for the next command.
Why both a prompt and a sentinel?
- The sentinel is the explicit end-of-output marker; LLDB does not provide a reliable boundary for arbitrary command output otherwise.
- The prompt is used to cleanly align the buffer for the next command after the sentinel is seen.
Annotated example (simplified):
- Backend writes:
thread backtracescript print("__XCODEBUILDMCP_DONE__")
- LLDB emits (illustrative):
... thread backtrace output ...__XCODEBUILDMCP_DONE__XCODEBUILDMCP_LLDB>
- Parser behavior:
- Sentinel marks the end of the command output payload.
- Prompt is used to trim the buffer, so the next command starts cleanly.
handleData()appends to an internal buffer, andcheckPending()scans for the sentinel regex/(^|\\r?\\n)__XCODEBUILDMCP_DONE__(\\r?\\n)/.- Output is the buffer up to the sentinel. The remainder is trimmed to the next prompt, if present.
sanitizeOutput()removes prompt echoes, sentinel lines, thescript print(...)lines, and empty lines, thenrunCommand()returnstrimEnd()output.
- Commands are serialized through a promise queue to avoid interleaved output.
waitForSentinel()rejects if a pending command exists, acting as a safety check.
- Startup timeout:
DEFAULT_STARTUP_TIMEOUT_MS = 10_000. - Per-command timeout:
DEFAULT_COMMAND_TIMEOUT_MS = 30_000(override viarunCommandopts). - Timeout failure clears the pending command and rejects the promise.
assertNoLldbError()throws if/error:/iappears in output (simple heuristic).- Process exit triggers
failPending(new Error(...))so in-flight calls fail promptly. runCommand()rejects immediately if the backend is already disposed.
getDefaultInteractiveSpawner() throws in test environments to prevent spawning real interactive
processes. Tests should inject a mock InteractiveSpawner into createLldbCliBackend() or a custom
DebuggerManager backend factory.
- Implementation:
src/utils/debugger/backends/dap-backend.ts, with protocol support insrc/utils/debugger/dap/transport.ts,src/utils/debugger/dap/types.ts, and adapter discovery insrc/utils/debugger/dap/adapter-discovery.ts. - Selected via backend selection (explicit
backend,XCODEBUILDMCP_DEBUGGER_BACKEND=dap, or default when unset). - Adapter discovery uses
xcrun --find lldb-dap; missing adapters raise a clear dependency error. - One
lldb-dapprocess is spawned per session; DAP framing and request correlation are handled byDapTransport. - Session handshake:
initialize→attach→configurationDone. - Breakpoints are stateful: adding/removing re-issues
setBreakpointsorsetFunctionBreakpointswith the remaining list. Conditions are passed in the request body. - Stack/variables typically require a stopped thread; the backend returns guidance if the process is still running.
- Simulator UUID resolution uses
xcrun simctl list devices available -j(determineSimulatorUuidinsrc/utils/simulator-utils.ts). - PID lookup uses
xcrun simctl spawn <simulatorId> launchctl list(resolveSimulatorAppPidinsrc/utils/debugger/simctl.ts).
- Attachment uses
xcrun lldb --no-lldbinitin the interactive backend. - Breakpoint conditions are applied internally by the LLDB CLI backend using
breakpoint modify -c "<condition>" <id>after creation.
- Debugging assumes a running simulator app.
- The typical flow is to build and launch via simulator tools (for example
build_sim), which usesexecuteXcodeBuildCommandto invokexcodebuild(orxcodemakewhen enabled). - After launch,
debug_attach_simattaches LLDB to the simulator process.
- Build and launch the app on a simulator (
build_sim,launch_app_sim). - Attach LLDB (
debug_attach_sim) using session defaults or explicit simulator + bundle ID. - Set breakpoints (
debug_breakpoint_add), inspect stack/variables (debug_stack,debug_variables), and issue arbitrary LLDB commands (debug_lldb_command). - Detach when done (
debug_detach).
- Tool entrypoints:
src/mcp/tools/debugging/* - Session defaults:
src/utils/session-store.ts - Debug session manager:
src/utils/debugger/debugger-manager.ts - Backends:
src/utils/debugger/backends/lldb-cli-backend.ts(default),src/utils/debugger/backends/dap-backend.ts - Interactive execution:
src/utils/execution/interactive-process.ts(used by LLDB CLI backend) - External commands:
xcrun simctl,xcrun lldb,xcodebuild