-
-
Notifications
You must be signed in to change notification settings - Fork 0
feat(docs): rewrite contributor and end-user pages #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.mdis a duplicate ofCLAUDE.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.Reviewed by Cursor Bugbot for commit 3db4301. Configure here.