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
61 changes: 61 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Project Rules

- Never use em dashes (—) in any content. Use commas, periods, colons, or parentheses instead.

## Docs site

### Audience
Two audiences share the public facing groups (Overview, Getting Started, Usage, Reference, Guides). Both sit on the public API side of XcodeBuildMCP:

- **End user**: a developer using a coding agent (via their MCP client) or the CLI to build iOS or macOS apps. XcodeBuildMCP extends their agent's capabilities.
- **Agent / MCP client**: the tool integrating with the MCP server.

Public docs cover the public API only: MCP spec features XcodeBuildMCP implements (structured content, resources, notifications, tool annotations, and so on), tools and what they do, MCP resources, configuration, session defaults, env vars, workflow management, CLI and its API, CLI and MCP output formats. Tool annotations (`readOnlyHint`, `destructiveHint`, `openWorldHint`) are part of the public MCP response, so document them in public pages, not Contributing.

The **Contributing** group is for people modifying XcodeBuildMCP itself. In scope: tool manifest files, tool authoring, internal architecture a contributor needs to add, edit, or remove a tool or workflow (rendering pipeline, tool registration, schemas, testing strategy). Contributors only need internals inside their authoring domain. Other internals do not need a doc home.

Placement rules when writing or moving content:

- An end user or MCP client needs it to use XcodeBuildMCP: public group.
- Only a contributor adding, editing, or removing a tool, workflow, or manifest needs it: Contributing.
- Neither: delete, do not invent a home for it.
- Frame public docs by user visible outcome, not implementation. Never leak source code literals (for example `{ sentry: true }` in server code, internal type names, private function names) into public pages.
- When a spec feature is part of the public MCP response, it belongs in public docs regardless of how MCP literate the reader is.

### Adding or editing a page

Content lives at `app/docs/_content/<slug>.mdx`. To add a new page, also update:

- `app/docs/_data/routes.ts`: add the slug to `DocSlug`, `PAGES_ORDER`, `PAGE_META`, and a `SIDEBAR_GROUPS` entry (top-level `items` or a `children` entry for sub-nav).
- `app/docs/_content/index.ts`: import the MDX and add it to `PAGE_COMPONENTS`.

### Available in every MDX file (no imports needed)

- `<Callout variant="info|warn|danger|success" title="...">body</Callout>`
- `<Tabs tabs={[{ label, content: <>...</> }, ...]} />`
- `<ToolExplorer />` (full tool catalog)
- `<LiveToolCount />`, `<LiveWorkflowCount />`, `<LiveVersion />`, `<LiveRef />`, `<LiveWorkflowToolCount workflow="..." />` (inline values)
- `<LiveWorkflowsTable />`, `<LiveChangelog limit={10} />`
- Fenced code blocks render with a copy button and language badge.

### Import explicitly in MDX when needed

- `<PageHeader breadcrumbs={[...]} title="..." lede="..." meta={[...]} />` from `../_components/page-header`
- `<Hero />` (intro only) from `../_components/hero`
- `<Icons.* />` from `../_components/icons`

### Dynamic data

- Prefer `<LiveToolCount />` etc. over hardcoding counts, workflow names, or versions. These pull from the latest release of `getsentry/XcodeBuildMCP` with a 1-hour revalidation window.
- Refresh the bundled fallback snapshot with `pnpm run docs:sync` when you need up-to-the-minute data during local development.

### Routing

- The introduction is served at `/docs`, not `/docs/introduction` (the latter 404s).
- Link to a heading with `/docs/<slug>#<kebab-heading>`. Heading ids are generated automatically.

### Commands

- `pnpm dev`: local dev server
- `pnpm build`: production build (static-generates every docs route)
- `pnpm run docs:sync`: refresh the bundled XcodeBuildMCP manifest snapshot
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AGENTS.md fully duplicates CLAUDE.md risking drift

Low Severity

The newly added AGENTS.md is a duplicate of CLAUDE.md. This creates a maintenance burden, as future updates to project rules or documentation will require manual synchronization across both files, risking inconsistent instructions for different AI tools.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3db4301. Configure here.

18 changes: 18 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@

## Docs site

### Audience
Two audiences share the public facing groups (Overview, Getting Started, Usage, Reference, Guides). Both sit on the public API side of XcodeBuildMCP:

- **End user**: a developer using a coding agent (via their MCP client) or the CLI to build iOS or macOS apps. XcodeBuildMCP extends their agent's capabilities.
- **Agent / MCP client**: the tool integrating with the MCP server.

Public docs cover the public API only: MCP spec features XcodeBuildMCP implements (structured content, resources, notifications, tool annotations, and so on), tools and what they do, MCP resources, configuration, session defaults, env vars, workflow management, CLI and its API, CLI and MCP output formats. Tool annotations (`readOnlyHint`, `destructiveHint`, `openWorldHint`) are part of the public MCP response, so document them in public pages, not Contributing.

The **Contributing** group is for people modifying XcodeBuildMCP itself. In scope: tool manifest files, tool authoring, internal architecture a contributor needs to add, edit, or remove a tool or workflow (rendering pipeline, tool registration, schemas, testing strategy). Contributors only need internals inside their authoring domain. Other internals do not need a doc home.

Placement rules when writing or moving content:

- An end user or MCP client needs it to use XcodeBuildMCP: public group.
- Only a contributor adding, editing, or removing a tool, workflow, or manifest needs it: Contributing.
- Neither: delete, do not invent a home for it.
- Frame public docs by user visible outcome, not implementation. Never leak source code literals (for example `{ sentry: true }` in server code, internal type names, private function names) into public pages.
- When a spec feature is part of the public MCP response, it belongs in public docs regardless of how MCP literate the reader is.

### Adding or editing a page

Content lives at `app/docs/_content/<slug>.mdx`. To add a new page, also update:
Expand Down
139 changes: 139 additions & 0 deletions app/docs/_components/mermaid-diagram.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"use client"

import { useEffect, useId, useMemo, useState } from "react"
import { Maximize2 } from "lucide-react"
import {
Dialog,
DialogContent,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"

interface MermaidDiagramProps {
source: string
}

const baseConfig = {
startOnLoad: false,
theme: "default" as const,
fontSize: 16,
flowchart: { useMaxWidth: false, htmlLabels: true, padding: 16 },
sequence: { useMaxWidth: false, actorFontSize: 15, noteFontSize: 14, messageFontSize: 14 },
// @ts-expect-error mermaid types lag the runtime config surface
stateDiagram: { useMaxWidth: false },
}

// Mermaid renders text labels via <foreignObject> + HTML, which inherit `color`
// from the page body. In dark-mode pages the body color is white, so the labels
// disappear against the diagram's light fills. Inject a scoped <style> into the
// SVG to lock text/label colors to a dark value regardless of page theme.
const colorLockStyle = `<style>
text { fill: #1f2937; }
.nodeLabel, .edgeLabel, .cluster-label, .titleText, .messageText, .noteText, .label, .stateLabel { color: #1f2937 !important; fill: #1f2937; }
.actor > tspan, .actor-line, .loopText, .loopText > tspan { fill: #1f2937; }
foreignObject, foreignObject div, foreignObject span, foreignObject p { color: #1f2937 !important; }
</style>`

function fitSvg(svg: string): string {
return svg
.replace(/(<svg\b[^>]*?)\sstyle="([^"]*?)"/, (_match, prefix, style) => {
const cleaned = style
.replace(/max-width:\s*[^;]+;?/g, "")
.replace(/^\s*;\s*/, "")
.trim()
return cleaned ? `${prefix} style="${cleaned}"` : prefix
})
.replace(/(<svg\b[^>]*?)\swidth="[^"]*"/, "$1")
.replace(/(<svg\b[^>]*?)\sheight="[^"]*"/, "$1")
.replace(/<svg\b/, '<svg width="100%" height="auto"')
.replace(/(<svg\b[^>]*?>)/, `$1${colorLockStyle}`)
}

export function MermaidDiagram({ source }: MermaidDiagramProps) {
const [svg, setSvg] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const [open, setOpen] = useState(false)
const baseId = useId()

const diagramId = useMemo(() => baseId.replace(/[^a-zA-Z0-9_-]/g, ""), [baseId])

useEffect(() => {
let isMounted = true

const renderDiagram = async () => {
try {
setSvg(null)
setError(null)

const mermaid = (await import("mermaid")).default
mermaid.initialize(baseConfig)

const { svg: rendered } = await mermaid.render(`mermaid-${diagramId}-${Date.now()}`, source)
if (isMounted) {
setSvg(fitSvg(rendered))
}
} catch (renderError) {
if (isMounted) {
setError(renderError instanceof Error ? renderError.message : "Unable to render Mermaid diagram")
}
}
}

if (!source.trim()) {
setSvg("")
return () => {
isMounted = false
}
}

void renderDiagram()

return () => {
isMounted = false
}
}, [diagramId, source])

if (error) {
return (
<div className="my-6">
<p className="mb-2 text-sm text-red-600 dark:text-red-400">Diagram failed to render: {error}</p>
<pre>
<code>{source}</code>
</pre>
</div>
)
}

if (svg === null) {
return <div className="my-6 h-40 w-full animate-pulse rounded-md bg-muted/50" aria-hidden="true" />
}

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<button
type="button"
aria-label="View diagram full screen"
className="mermaid-diagram group relative my-6 block w-full cursor-zoom-in overflow-x-auto rounded-md border border-neutral-200 bg-white p-4 text-left text-neutral-900 shadow-sm transition-shadow hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<span dangerouslySetInnerHTML={{ __html: svg }} />
<span
aria-hidden="true"
className="pointer-events-none absolute right-2 top-2 text-neutral-400 opacity-40 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100"
>
<Maximize2 className="h-4 w-4" />
</span>
</button>
</DialogTrigger>
<DialogContent className="grid h-[90vh] w-[95vw] max-w-[95vw] grid-rows-[auto_1fr] gap-0 overflow-hidden border-neutral-200 bg-white p-0 text-neutral-900 sm:rounded-lg">
<DialogTitle className="border-b border-neutral-200 px-4 py-3 text-sm font-medium text-neutral-900">
Diagram
</DialogTitle>
<div
className="mermaid-diagram-fullscreen flex h-full w-full items-center justify-center overflow-auto bg-white p-6"
dangerouslySetInnerHTML={{ __html: svg }}
/>
</DialogContent>
</Dialog>
)
}
2 changes: 1 addition & 1 deletion app/docs/_components/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function Tabs({ tabs, initial = 0 }: TabsProps) {
</button>
))}
</div>
<div>{tabs[i]?.content}</div>
<div className="tabs-content">{tabs[i]?.content}</div>
</>
)
}
123 changes: 123 additions & 0 deletions app/docs/_content/architecture-daemon.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { PageHeader } from "../_components/page-header"

<PageHeader
breadcrumbs={["Docs", "Contributing", "Architecture", "Daemon Lifecycle"]}
title="Daemon Lifecycle"
lede="Why XcodeBuildMCP keeps one small background process per workspace for tool work that needs state after the shell command exits, and how that process starts, runs, and shuts down."
/>

Some tool work has to outlive the shell command that started it — an active debug session, an in-progress video capture, an open Xcode bridge. The CLI process exits as soon as the shell command finishes, so that state has nowhere to live. This page is about the per-workspace background process that owns that kind of work, and the lifecycle that decides when it starts, what it owns, and when it shuts down.

## Terms used here

See the full glossary at [Core terms](/docs/architecture#core-terms).

- **daemon** — The workspace-scoped background process (`xcodebuildmcp daemon`) that owns stateful tool work — debug sessions, video captures, long-running SwiftPM work, the Xcode IDE bridge — across short-lived CLI commands.
- **transport** — The wire a request travels on; for the daemon, that is a Unix socket scoped to the workspace.
- **tool handler** — The shared function the daemon hosts on behalf of stateful tools; the same function the in-process CLI would have called.
- **workspace root** — The project root that owns configuration and daemon state, used to derive a stable key for the daemon socket.

## Why the daemon exists

CLI processes are short-lived. That is good for scripts, but bad for work that needs state after the command exits. Debug sessions, video recording, background Swift Package work, log capture, and the Xcode IDE bridge all need an owner that survives one shell command.

The daemon is that owner. The CLI still provides the user-facing command surface and output mode. The daemon owns stateful execution, streams fragments back to the CLI, and returns final structured output when the tool finishes or the stateful action reaches its response boundary.

## Lifecycle

```mermaid
stateDiagram-v2
[*] --> NotRunning

NotRunning --> AutoStarting: first stateful CLI invocation
AutoStarting --> Listening: daemon start succeeds
AutoStarting --> StartFailed: startup timeout or launch failure
StartFailed --> NotRunning: user retries or starts manually

Listening --> HandlingRequest: tool.invoke or xcode-ide.invoke
HandlingRequest --> Streaming: fragments emitted
Streaming --> HandlingRequest: more fragments
HandlingRequest --> Listening: final result frame sent

Listening --> IdleCandidate: idle timeout elapsed
IdleCandidate --> Listening: in-flight requests > 0
IdleCandidate --> Listening: active runtime sessions remain
IdleCandidate --> GracefulShutdown: no in-flight requests and no active sessions

Listening --> GracefulShutdown: daemon.stop
Listening --> GracefulShutdown: SIGTERM / SIGINT

GracefulShutdown --> Cleanup: close server, remove registry, remove socket
Cleanup --> NotRunning

NotRunning --> [*]
```

## Workspace scoping

Each daemon is scoped to one workspace. XcodeBuildMCP derives the workspace identity from the project config location when `.xcodebuildmcp/config.yaml` exists. Otherwise it uses the current directory. That workspace root becomes a stable key for the daemon socket path.

| Concept | Meaning |
|---------|---------|
| Workspace root | The project root that owns config and daemon state. |
| Workspace key | A stable derived key used to separate daemon instances. |
| Socket path | The local Unix socket the CLI uses to talk to that workspace daemon. |

This avoids sharing debugger state, video sessions, or bridge state across unrelated projects.

## Startup and routing

A tool opts into daemon routing through manifest routing metadata. When the CLI invokes a stateful tool, the invoker checks whether the workspace daemon is already running. If it is not, the invoker starts it, waits for the socket, and then sends the tool invocation over the daemon protocol.

The user-facing command does not change:

```shell
xcodebuildmcp simulator record-video --simulator-id <UDID> --output-path ./session.mp4
```

The routing choice is internal. The shell still sees CLI output in the requested mode.

## Protocol shape

The daemon protocol has two jobs:

1. Forward progress fragments back to the CLI while the daemon-owned handler runs.
2. Return the final structured output and next-step data when the invocation reaches a response boundary.

That mirrors direct CLI invocation. The difference is process ownership: direct tools run in the CLI process, daemon-routed tools run in the workspace daemon and stream events back to the CLI client.

## Idle shutdown

The default idle timeout is 10 minutes. It can be overridden with `XCODEBUILDMCP_DAEMON_IDLE_TIMEOUT_MS`.

The daemon shuts down only when all of these are true:

| Gate | Why it matters |
|------|----------------|
| Idle timeout elapsed | Avoids stopping immediately between related commands. |
| No in-flight requests | Avoids killing an active invocation before it sends its final frame. |
| No active runtime sessions | Avoids killing stateful sessions that still own work. |

This is stricter than just checking for an empty session list. It protects both active protocol requests and longer-lived runtime sessions.

## Manual control

The CLI exposes daemon commands for inspection and recovery:

```shell
xcodebuildmcp daemon status
xcodebuildmcp daemon start
xcodebuildmcp daemon stop
xcodebuildmcp daemon restart
xcodebuildmcp daemon list
xcodebuildmcp daemon logs
```

Use manual control for debugging. Normal stateful tool calls auto-start the daemon and do not require setup.

## Related

- [CLI](/docs/cli#per-workspace-daemon), user-facing daemon behavior
- [Environment Variables](/docs/env-vars), daemon and startup overrides
- [Troubleshooting](/docs/troubleshooting), common local failures
- [Runtime Boundaries](/docs/architecture-runtime-boundaries#direct-vs-daemon-routed-tools), why daemon routing belongs to CLI
Loading
Loading