diff --git a/app/docs/_content/architecture-debugging.mdx b/app/docs/_content/architecture-debugging.mdx new file mode 100644 index 0000000..9d06ae2 --- /dev/null +++ b/app/docs/_content/architecture-debugging.mdx @@ -0,0 +1,159 @@ +import { PageHeader } from "../_components/page-header" + + + +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 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 diff --git a/app/docs/_content/architecture.mdx b/app/docs/_content/architecture.mdx index 0494813..8968e8a 100644 --- a/app/docs/_content/architecture.mdx +++ b/app/docs/_content/architecture.mdx @@ -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 diff --git a/app/docs/_content/index.ts b/app/docs/_content/index.ts index 98cc04a..727eecf 100644 --- a/app/docs/_content/index.ts +++ b/app/docs/_content/index.ts @@ -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 = { @@ -62,6 +64,8 @@ export const PAGE_COMPONENTS: Record = { "architecture-tool-lifecycle": ArchitectureToolLifecyclePage, "architecture-rendering-output": ArchitectureRenderingOutputPage, "architecture-daemon": ArchitectureDaemonPage, + "architecture-debugging": ArchitectureDebuggingPage, "tool-authoring": ToolAuthoringPage, + "schema-versioning": SchemaVersioningPage, testing: TestingPage, } diff --git a/app/docs/_content/schema-versioning.mdx b/app/docs/_content/schema-versioning.mdx new file mode 100644 index 0000000..f18916d --- /dev/null +++ b/app/docs/_content/schema-versioning.mdx @@ -0,0 +1,206 @@ +import { PageHeader } from "../_components/page-header" + + + +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 diff --git a/app/docs/_content/tool-authoring.mdx b/app/docs/_content/tool-authoring.mdx index 63fa531..62c821d 100644 --- a/app/docs/_content/tool-authoring.mdx +++ b/app/docs/_content/tool-authoring.mdx @@ -20,7 +20,6 @@ If you are new to the architecture, this page leans on a few terms that are defi | Workflow manifest | `manifests/workflows/.yaml` | References the tool ID so the tool appears in one or more workflows. | | Output schema | `schemas/structured-output//.schema.json` | Validates the structured JSON response returned through MCP `structuredContent` and CLI JSON output. | | Fixtures | `src/snapshot-tests/__fixtures__/{mcp,cli,json}/...` | Lock the MCP text, CLI text, and JSON response contracts. | -| Generated docs | `docs/TOOLS.md`, `docs/TOOLS-CLI.md` | Generated from manifests and schemas. Do not edit them by hand. | The final structured result is the canonical output. Rendered text is derived from that result, fragments, and runtime-specific rendering rules. See [Tool Lifecycle](/docs/architecture-tool-lifecycle#streaming-vs-non-streaming-tools) for the runtime model and [Output Formats](/docs/output-formats) for the response shape. @@ -159,12 +158,11 @@ tools: A tool can appear in multiple workflows, but it should have one tool manifest. Workflow selection and predicates decide which runtimes expose it. -### 6. Regenerate docs and validate schemas +### 6. Validate docs and schemas If you add, remove, or change tool metadata, run: ```shell -npm run docs:update npm run docs:check ``` @@ -175,8 +173,6 @@ npm run test:schema-fixtures npx vitest run src/core/__tests__/structured-output-schema.test.ts ``` -`npm run docs:update` updates generated tool reference files. Do not hand-edit generated tool docs. - ### 7. Add fixtures Add or update representative fixtures under: @@ -327,7 +323,7 @@ Use this pipeline only when the tool wraps `xcodebuild`. For any other long-runn { label: "Metadata", content: ( <>

Change manifests/tools/<tool>.yaml first. Metadata includes descriptions, names, annotations, availability, predicates, routing, next-step templates, and output schema metadata.

-

Run npm run docs:update and npm run docs:check. If a name changes, update tests, fixtures, and any next-step references that call the old name.

+

Run npm run docs:check. If a name changes, update tests, fixtures, and any next-step references that call the old name.

) }, { label: "Input schema", content: ( @@ -346,8 +342,8 @@ Use this pipeline only when the tool wraps `xcodebuild`. For any other long-runn | Change | Required follow-up | |--------|--------------------| -| Metadata only | Regenerate generated docs and update fixture text if descriptions or names are visible. | -| Input schema | Update Zod schema, parameter tests, generated docs, and any snapshots affected by validation text. | +| Metadata only | Run docs checks and update fixture text if descriptions or names are visible. | +| Input schema | Update Zod schema, parameter tests, docs checks, and any snapshots affected by validation text. | | Compatible output addition | Update the existing schema, implementation, JSON fixtures, and schema fixture tests. | | Breaking output change | Add `schemas/structured-output//2.schema.json`, emit `schemaVersion: '2'`, update manifest `outputSchema.version`, and update fixtures. | | Runtime behavior | Update logic tests, MCP text fixtures, CLI text fixtures, JSON fixtures, and changelog if user-facing. | @@ -365,7 +361,7 @@ Use a deletion checklist. Tool removal affects user-visible surfaces. 3. Delete the implementation file if no other code imports it. 4. Delete tests that only cover that tool. 5. Delete MCP, CLI, and JSON fixtures for that tool. -6. Run `npm run docs:update` and `npm run docs:check`. +6. Run `npm run docs:check`. 7. Run `npm run typecheck`, `npm test`, and `npm run test:schema-fixtures`. Do not delete a shared schema just because one tool stopped using it. Schemas are published contracts. Remove one only when it is unpublished or clearly unused after checking consumers. @@ -379,7 +375,6 @@ Do not delete a shared schema just because one tool stopped using it. Schemas ar - Using `createStreamingExecutionContext(...)` in a non-streaming tool. - Relying on streamed fragments for final JSON data. - Changing JSON payload shape without updating schemas and JSON fixtures. -- Hand-editing generated tool docs. - Updating snapshots before understanding why they changed. - Preserving legacy fallback behavior instead of making the requested path canonical. diff --git a/app/docs/_data/routes.ts b/app/docs/_data/routes.ts index a81ac6e..c1e279f 100644 --- a/app/docs/_data/routes.ts +++ b/app/docs/_data/routes.ts @@ -30,7 +30,9 @@ export type DocSlug = | "architecture-tool-lifecycle" | "architecture-rendering-output" | "architecture-daemon" + | "architecture-debugging" | "tool-authoring" + | "schema-versioning" | "testing" export interface DocRoute { @@ -82,7 +84,9 @@ export const PAGES_ORDER: DocSlug[] = [ "architecture-tool-lifecycle", "architecture-rendering-output", "architecture-daemon", + "architecture-debugging", "tool-authoring", + "schema-versioning", "testing", ] @@ -267,12 +271,24 @@ export const PAGE_META: Record = { group: "Contributing", description: "Why stateful CLI tools use a per-workspace daemon and how its transport lifecycle works.", }, + "architecture-debugging": { + slug: "architecture-debugging", + title: "Debugging", + group: "Contributing", + description: "How simulator debugging sessions, DAP, and LLDB CLI backends are wired.", + }, "tool-authoring": { slug: "tool-authoring", title: "Tool Authoring", group: "Contributing", description: "Add, modify, or remove a tool end to end.", }, + "schema-versioning": { + slug: "schema-versioning", + title: "Schema Versioning", + group: "Contributing", + description: "How structured output JSON Schemas are versioned and published.", + }, testing: { slug: "testing", title: "Testing", @@ -339,9 +355,11 @@ export const SIDEBAR_GROUPS: SidebarGroup[] = [ "architecture-tool-lifecycle", "architecture-rendering-output", "architecture-daemon", + "architecture-debugging", ], }, { slug: "tool-authoring" }, + { slug: "schema-versioning" }, { slug: "testing" }, ], }, diff --git a/app/page.tsx b/app/page.tsx index f0b4152..e3481df 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -773,7 +773,7 @@ sessionDefaults: View Issues