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
159 changes: 159 additions & 0 deletions app/docs/_content/architecture-debugging.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { PageHeader } from "../_components/page-header"

<PageHeader
breadcrumbs={["Docs", "Contributing", "Architecture", "Debugging"]}
title="Debugging Architecture"
lede="How simulator debugging tools attach to running apps, keep sessions alive, and route debugger operations through the DAP and LLDB CLI backends."
/>

This page is for contributors working on the debugging workflow. It covers how the public debugging tools are wired to the shared debugger manager, how sessions are kept alive, and how the two backend implementations map tool calls to debugger operations.

## Scope

| Area | Files |
|------|-------|
| Tool entrypoints | `src/mcp/tools/debugging/*` |
| Debugger subsystem | `src/utils/debugger/*` |
| Backend implementations | `src/utils/debugger/backends/dap-backend.ts`, `src/utils/debugger/backends/lldb-cli-backend.ts` |
| DAP protocol support | `src/utils/debugger/dap/*` |
| External execution | `src/utils/execution/*`, `xcrun simctl`, `xcrun lldb`, `lldb-dap`, `xcodebuild` |

## Registration and wiring

Debugging tools are loaded through workflow manifests, like every other tool group. Runtime visibility decides whether the debugging workflow is exposed, then tool handlers are created through the typed tool factory.

Debugging tools use two factory shapes:

- `createTypedToolWithContext` for standard tools with Zod validation and dependency injection.
- `createSessionAwareToolWithContext` for tools that merge session defaults before validation.

The injected debugging context provides:

| Dependency | Purpose |
|------------|---------|
| `executor` | Runs external commands such as `simctl` and adapter discovery. |
| `debugger` | Shared `DebuggerManager` instance that owns sessions and backend routing. |

`debug_attach_sim` is session-aware, so callers can omit simulator selectors when session defaults provide them.

## Session lifecycle

`DebuggerManager` owns lifecycle, state, and backend selection.

Backend selection order:

1. Explicit backend argument on the attach call.
2. `XCODEBUILDMCP_DEBUGGER_BACKEND` or `debuggerBackend` from resolved config.
3. Default config value, currently `dap`.

Accepted backend values are:

| Value | Backend |
|-------|---------|
| `dap` | `lldb-dap` Debug Adapter Protocol backend. |
| `lldb-cli` | Long-lived `xcrun lldb` command-line backend. |
| `lldb` | Normalized to `lldb-cli` when read from config. |

Attach flow:

1. `debug_attach_sim` resolves simulator UUID and target process ID.
2. It calls `DebuggerManager.createSession` with simulator ID, PID, and backend preference if supplied.
3. The manager creates the selected backend and calls `backend.attach`.
4. On success, the manager stores session metadata and marks the current session.
5. Follow-up tools resolve an explicit session ID or use the current session.
6. `debug_detach` calls `DebuggerManager.detachSession`, which detaches and disposes the backend.

## Tool to backend mapping

| MCP tool | Manager operation | DAP request mapping | LLDB CLI behavior |
|----------|-------------------|---------------------|-------------------|
| `debug_attach_sim` | `createSession` then `attach` | `initialize`, `attach`, `configurationDone` | Spawns `xcrun lldb`, initializes prompt/sentinel parsing, attaches to PID. |
| `debug_lldb_command` | `runCommand` | `evaluate` with `context: "repl"` | Writes the command to the interactive LLDB process. |
| `debug_stack` | `getStack` | `threads`, then `stackTrace` | Runs an LLDB stack command and sanitizes output. |
| `debug_variables` | `getVariables` | `threads`, `stackTrace`, `scopes`, `variables` | Runs LLDB variable inspection and sanitizes output. |
| `debug_breakpoint_add` | `addBreakpoint` | `setBreakpoints` or `setFunctionBreakpoints` | Creates an LLDB breakpoint and applies conditions internally. |
| `debug_breakpoint_remove` | `removeBreakpoint` | Reissues breakpoint lists without the removed entry. | Removes the LLDB breakpoint by ID. |
| `debug_detach` | `detachSession` | `disconnect` | Detaches and disposes the LLDB process. |

## DAP backend

The DAP backend is implemented by `src/utils/debugger/backends/dap-backend.ts`. It starts one `lldb-dap` subprocess per debug session and talks to it over the Debug Adapter Protocol.

Supporting modules:

| Module | Purpose |
|--------|---------|
| `src/utils/debugger/dap/types.ts` | Minimal DAP types used by the backend. |
| `src/utils/debugger/dap/transport.ts` | `Content-Length` framing, request correlation, event handling, and process disposal. |
| `src/utils/debugger/dap/adapter-discovery.ts` | Resolves `lldb-dap` with `xcrun --find lldb-dap` and reports dependency errors. |

Lifecycle details:

- Adapter discovery happens before the backend attaches.
- Missing `lldb-dap` raises a dependency error that tells the user to install/configure Xcode or switch to `lldb-cli`.
- The transport supports concurrent DAP requests by correlating request sequence IDs.
- Backend state mutations, such as breakpoint registry updates, are serialized where needed.
- `dispose()` is best-effort and must not throw, because attach failure cleanup calls it.

### Breakpoint strategy

DAP breakpoint removal is stateful. The adapter does not remove one breakpoint by ID directly. Instead, the backend keeps registries and reissues the complete remaining set for that source or function list:

- File/line breakpoints are grouped by source path and sent through `setBreakpoints`.
- Function breakpoints are sent through `setFunctionBreakpoints`.
- Conditions are part of the breakpoint request body.
- The backend stores returned IDs so later `debug_breakpoint_remove` calls can find and remove the right registry entry.

This is why conditional breakpoint handling belongs inside the backend API rather than as an extra LLDB command after creation.

### Stack and variables

DAP stack and variable requests usually require a stopped thread. If the target is still running, the backend returns guidance instead of pretending stack data is available. The normal flow is to set a breakpoint, trigger it, then call stack or variable tools after the process stops.

## LLDB CLI backend

The LLDB CLI backend is implemented by `src/utils/debugger/backends/lldb-cli-backend.ts`. It keeps one long-lived `xcrun lldb` process per session.

The backend uses an interactive process model:

1. Spawn `xcrun lldb --no-lldbinit` with a custom prompt.
2. Write a command.
3. Write a sentinel command that prints `__XCODEBUILDMCP_DONE__`.
4. Buffer stdout and stderr until the sentinel appears.
5. Trim the buffer to the next prompt.
6. Remove prompt echoes, sentinel lines, and helper command echoes.
7. Return the sanitized command output.

The prompt indicates the REPL is ready for the next command. The sentinel provides an explicit end-of-output marker for arbitrary LLDB command output.

Commands are serialized through a queue so two tool calls cannot interleave output in the same LLDB process.

## External tool invocation

| External tool | Used for |
|---------------|----------|
| `xcrun simctl list devices available -j` | Resolve simulator names to UUIDs. |
| `xcrun simctl spawn <simulatorId> launchctl list` | Resolve a simulator app process ID by bundle ID. |
| `xcrun --find lldb-dap` | Locate the DAP adapter. |
| `xcrun lldb` | Run the LLDB CLI backend. |
| `xcodebuild` | Build and launch context before attaching. |

Debugging assumes a running simulator app. The common user flow is to build and launch with simulator tools, attach with `debug_attach_sim`, inspect or control the process, then detach.

## Testing and injection

Debugger code must stay test-safe. Default executors and spawners throw under Vitest, so tests inject fakes instead of spawning real debugger processes.

Useful test seams:

- Inject a custom backend factory into `DebuggerManager` for selection and lifecycle tests.
- Inject a fake command executor for adapter discovery.
- Inject a fake interactive spawner for DAP transport and LLDB CLI backend tests.
- Keep `dispose()` idempotent and non-throwing so failure-path tests can clean up reliably.

## Related

- [Daemon Lifecycle](/docs/architecture-daemon), why debug sessions route through the daemon in CLI mode
- [Runtime Boundaries](/docs/architecture-runtime-boundaries), how MCP and CLI share tool handlers
- [Tool Authoring](/docs/tool-authoring), handler and manifest conventions
- [Configuration](/docs/configuration), user-facing debugger backend settings
1 change: 1 addition & 0 deletions app/docs/_content/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ flowchart TB
- [Tool Lifecycle](/docs/architecture-tool-lifecycle) — read this next when you are writing or modifying a handler and need the contract from validated input through fragments, structured output, and next steps.
- [Rendering & Output](/docs/architecture-rendering-output) — read this next when you want to know how fragments and structured output become MCP text, CLI text, JSON, JSONL, or a raw transcript.
- [Daemon Lifecycle](/docs/architecture-daemon) — read this next when you are touching stateful work — debugging, video capture, long-running SwiftPM, or the Xcode IDE bridge — and need the daemon's transport and lifecycle behavior.
- [Debugging](/docs/architecture-debugging) — read this next when you are changing simulator debugger tools, debug session lifecycle, or the DAP and LLDB CLI backends.

## Build pipeline

Expand Down
4 changes: 4 additions & 0 deletions app/docs/_content/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ import ArchitectureManifestVisibilityPage from "./architecture-manifest-visibili
import ArchitectureToolLifecyclePage from "./architecture-tool-lifecycle.mdx"
import ArchitectureRenderingOutputPage from "./architecture-rendering-output.mdx"
import ArchitectureDaemonPage from "./architecture-daemon.mdx"
import ArchitectureDebuggingPage from "./architecture-debugging.mdx"
import ToolAuthoringPage from "./tool-authoring.mdx"
import SchemaVersioningPage from "./schema-versioning.mdx"
import TestingPage from "./testing.mdx"

export const PAGE_COMPONENTS: Record<DocSlug, ComponentType> = {
Expand Down Expand Up @@ -62,6 +64,8 @@ export const PAGE_COMPONENTS: Record<DocSlug, ComponentType> = {
"architecture-tool-lifecycle": ArchitectureToolLifecyclePage,
"architecture-rendering-output": ArchitectureRenderingOutputPage,
"architecture-daemon": ArchitectureDaemonPage,
"architecture-debugging": ArchitectureDebuggingPage,
"tool-authoring": ToolAuthoringPage,
"schema-versioning": SchemaVersioningPage,
testing: TestingPage,
}
206 changes: 206 additions & 0 deletions app/docs/_content/schema-versioning.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import { PageHeader } from "../_components/page-header"

<PageHeader
breadcrumbs={["Docs", "Contributing", "Schema Versioning"]}
title="Schema Versioning"
lede="How XcodeBuildMCP versions, publishes, and maintains structured output JSON Schemas served from xcodebuildmcp.com."
/>

XcodeBuildMCP publishes structured output schemas at stable website URLs. External consumers can validate CLI JSON output and MCP `structuredContent` against those schemas, so schema changes need the same care as any public contract.

## Goals

- Keep schema contracts stable and predictable for external consumers.
- Make published schema URLs real, durable, and safe to reference.
- Serve schemas directly from `https://xcodebuildmcp.com/schemas/...`.
- Avoid ambiguous compatibility rules.

## Canonical schema identity

Each structured payload has two stable identifiers:

```json
{
"schema": "xcodebuildmcp.output.build-result",
"schemaVersion": "1"
}
```

The matching published schema URL is:

```text
https://xcodebuildmcp.com/schemas/structured-output/xcodebuildmcp.output.build-result/1.schema.json
```

The in-payload `schema` and `schemaVersion` values must always match the published schema document that validates that payload.

## Version format

`schemaVersion` uses integer strings only:

- `"1"`
- `"2"`
- `"3"`

Do not use semver-style schema versions such as `"1.1"` or `"2.0"`. The version number is a contract version, not a release number.

## Versioning rules

### Published versions are immutable

Once a schema version is published, do not make breaking changes to that file.

Breaking changes include:

- Removing a property.
- Making an optional property required.
- Narrowing allowed values or enums.
- Changing object shape incompatibly.
- Changing field meaning in a way that could break clients.

If any of those changes are needed, publish a new version instead:

```text
schemas/structured-output/xcodebuildmcp.output.build-result/2.schema.json
```

Then emit:

```json
"schemaVersion": "2"
```

### Old versions remain available

Previously published schema files must continue to be hosted. Do not remove old schema versions from the website once consumers may rely on them.

### Additive changes

Additive optional fields can be compatible, but use caution. If a new field is truly optional and old clients can safely ignore it, it may remain within the same schema version. If there is any doubt about compatibility or meaning, bump the schema version.

Bias toward a new version when the contract meaning changes.

## Directory layout

Source schemas in the main repository live under:

```text
schemas/structured-output/
```

Published schemas on the website live under:

```text
public/schemas/structured-output/
```

A source file such as:

```text
schemas/structured-output/xcodebuildmcp.output.build-result/1.schema.json
```

is published to:

```text
public/schemas/structured-output/xcodebuildmcp.output.build-result/1.schema.json
```

The website then serves it at:

```text
https://xcodebuildmcp.com/schemas/structured-output/xcodebuildmcp.output.build-result/1.schema.json
```

## Publishing workflow

Schema publishing is handled from the main repository by a GitHub Actions workflow.

Trigger conditions:

- Push to `main` when files under `schemas/**` change.
- Manual `workflow_dispatch`.

Publishing steps:

1. Check out `getsentry/XcodeBuildMCP`.
2. Clone `getsentry/xcodebuildmcp.com` over SSH.
3. Sync `schemas/structured-output/` into `public/schemas/structured-output/` in the website repository.
4. Commit the website change if the published files changed.
5. Push to the website repository `main` branch.
6. Let Vercel deploy the website normally.

This keeps schema authoring in the main project repository while using the website repository as the deployment surface.

## Required secret

The publishing workflow requires this repository secret:

```text
XCODEBUILDMCP_WEBSITE_DEPLOY_KEY
```

This secret must contain an SSH private key with write access to:

```text
git@github.com:getsentry/xcodebuildmcp.com.git
```

The corresponding public key should be installed as a deploy key on the website repository with write access.

### Deploy key setup

1. Generate a dedicated SSH key pair for schema publishing.

```shell
ssh-keygen -t ed25519 -C "schema-publisher" -f ./xcodebuildmcp-website-deploy-key
```

2. In `getsentry/xcodebuildmcp.com`, add the public key as a deploy key with write access.
3. In `getsentry/XcodeBuildMCP`, add the private key as an actions secret named `XCODEBUILDMCP_WEBSITE_DEPLOY_KEY`.
4. Trigger the `Publish Schemas` workflow manually once to verify SSH access and sync.
5. Confirm that the website repository receives the commit and Vercel deploys it.
6. Confirm a final URL resolves, for example:

```text
https://xcodebuildmcp.com/schemas/structured-output/xcodebuildmcp.output.build-result/1.schema.json
```

Use a dedicated deploy key for this workflow only. Do not reuse a personal SSH key.

## Consumer guidance

Consumers should branch on both `schema` and `schemaVersion`:

```ts
switch (`${payload.schema}@${payload.schemaVersion}`) {
case "xcodebuildmcp.output.build-result@1":
// validate using v1 schema
break
case "xcodebuildmcp.output.build-result@2":
// validate using v2 schema
break
default:
throw new Error("Unsupported schema version")
}
```

These JSON Schemas describe payload shapes. They are not an OpenAPI description by themselves. If an HTTP API is introduced later, OpenAPI should reference the schema files as component schemas instead of trying to infer endpoints from them.

## Maintenance checklist

When updating schemas:

1. Decide whether the change is compatible or breaking.
2. If breaking, add a new versioned schema file instead of changing the old one.
3. Update fixture payloads to emit the correct `schemaVersion`.
4. Run `npm run test:schema-fixtures`.
5. Merge to `main`.
6. Confirm the publish workflow updated the website repo.
7. Confirm the final schema URL resolves on the website.

## Related

- [Tool Authoring](/docs/tool-authoring), where schemas fit into a tool change
- [Output Formats](/docs/output-formats), how structured output appears in MCP and CLI responses
- [Testing](/docs/testing), schema fixture validation
Loading
Loading