diff --git a/.gitignore b/.gitignore index f650315..d47331f 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,7 @@ yarn-error.log* # typescript *.tsbuildinfo -next-env.d.ts \ No newline at end of file +next-env.d.ts + +# local demo backups +.demo-backups/ \ No newline at end of file diff --git a/app/docs/_components/schema-explorer.tsx b/app/docs/_components/schema-explorer.tsx new file mode 100644 index 0000000..62796ff --- /dev/null +++ b/app/docs/_components/schema-explorer.tsx @@ -0,0 +1,106 @@ +"use client" + +import { useMemo, useState } from "react" +import { Icons } from "./icons" +import { publishedSchemas } from "../_data/generated/schemas" + +const latestVersionOption = "latest" + +export function SchemaExplorer() { + const [query, setQuery] = useState("") + const [selectedVersion, setSelectedVersion] = useState(latestVersionOption) + + const versionOptions = useMemo(() => { + const versions = new Set() + + for (const schema of publishedSchemas) { + for (const version of schema.versions) { + versions.add(version) + } + } + + return Array.from(versions).sort((left, right) => Number(right) - Number(left)) + }, []) + + const filteredSchemas = useMemo(() => { + const normalizedQuery = query.trim().toLowerCase() + + return publishedSchemas.filter((schema) => { + const matchesQuery = normalizedQuery + ? schema.name.toLowerCase().includes(normalizedQuery) + : true + const hasSelectedVersion = + selectedVersion === latestVersionOption || schema.versions.includes(selectedVersion) + + return matchesQuery && hasSelectedVersion + }) + }, [query, selectedVersion]) + + return ( + <> +
+
+
+ +
+ setQuery(event.target.value)} + placeholder={`Filter ${publishedSchemas.length} schemas…`} + /> +
+ +
+ +
+ {filteredSchemas.map((schema) => { + const resolvedVersion = + selectedVersion === latestVersionOption ? schema.latest : selectedVersion + const href = `/schemas/structured-output/${encodeURIComponent( + schema.name, + )}/${encodeURIComponent(resolvedVersion)}.schema.json` + const versionLabel = + selectedVersion === latestVersionOption + ? `latest · v${schema.latest}` + : `v${resolvedVersion}` + + return ( + +
+
{schema.name}
+
{versionLabel}
+
+ JSON schema +
+ ) + })} + {filteredSchemas.length === 0 ? ( +
+ No schemas match the current filter and version. +
+ ) : null} +
+ + ) +} diff --git a/app/docs/_content/index.ts b/app/docs/_content/index.ts index 35d22a2..8a0b049 100644 --- a/app/docs/_content/index.ts +++ b/app/docs/_content/index.ts @@ -10,6 +10,7 @@ import WorkflowsPage from "./workflows.mdx" import ToolsPage from "./tools.mdx" import MCPProtocolSupportPage from "./mcp-protocol-support.mdx" import OutputFormatsPage from "./output-formats.mdx" +import SchemasPage from "./schemas.mdx" import ConfigurationPage from "./configuration.mdx" import SessionDefaultsPage from "./session-defaults.mdx" import EnvVarsPage from "./env-vars.mdx" @@ -46,6 +47,7 @@ export const PAGE_COMPONENTS: Record = { tools: ToolsPage, "mcp-protocol-support": MCPProtocolSupportPage, "output-formats": OutputFormatsPage, + schemas: SchemasPage, configuration: ConfigurationPage, "session-defaults": SessionDefaultsPage, "env-vars": EnvVarsPage, diff --git a/app/docs/_content/output-formats.mdx b/app/docs/_content/output-formats.mdx index 7b8489f..bcd4624 100644 --- a/app/docs/_content/output-formats.mdx +++ b/app/docs/_content/output-formats.mdx @@ -320,10 +320,7 @@ Parameterized Swift Testing groups currently surface as a single aggregate entry ## Response schema reference -Canonical JSON schemas live in the source repository under [`schemas/structured-output/`](https://github.com/getsentry/XcodeBuildMCP/tree/main/schemas/structured-output). Concrete examples: - -- [`xcodebuildmcp.output.simulator-list/2.schema.json`](https://github.com/getsentry/XcodeBuildMCP/blob/main/schemas/structured-output/xcodebuildmcp.output.simulator-list/2.schema.json) -- [`xcodebuildmcp.output.build-result/2.schema.json`](https://github.com/getsentry/XcodeBuildMCP/blob/main/schemas/structured-output/xcodebuildmcp.output.build-result/2.schema.json) +Canonical JSON schemas are published at stable website URLs under `/schemas/structured-output/...`. Browse the full list, including available versions for each schema family, in [Published Schemas](/docs/schemas). ## Examples @@ -455,4 +452,5 @@ Canonical JSON schemas live in the source repository under [`schemas/structured- - [MCP Server Mode](/docs/mcp-mode), stdio server behavior - [Tools Reference](/docs/tools), generated tool catalog - [Rendering & Output](/docs/architecture-rendering-output), contributor-level rendering model +- [Published Schemas](/docs/schemas), browsable JSON Schema list with version links - [Tool Authoring](/docs/tool-authoring), adding schemas and structured results diff --git a/app/docs/_content/schemas.mdx b/app/docs/_content/schemas.mdx new file mode 100644 index 0000000..5e13977 --- /dev/null +++ b/app/docs/_content/schemas.mdx @@ -0,0 +1,32 @@ +import { PageHeader } from "../_components/page-header" +import { SchemaExplorer } from "../_components/schema-explorer" + + + +XcodeBuildMCP tool results include a `schema` identifier and a `schemaVersion` string. Use those fields together to choose the published schema that validates the `data` payload returned by CLI JSON output or MCP `structuredContent`. + +Each link below points at the raw JSON Schema served from `/schemas/structured-output/...`. + + + +## URL format + +```text +https://xcodebuildmcp.com/schemas/structured-output//.schema.json +``` + +For example: + +```text +https://xcodebuildmcp.com/schemas/structured-output/xcodebuildmcp.output.build-result/2.schema.json +``` + +## Related + +- [Output Formats](/docs/output-formats), where structured output appears in CLI and MCP responses +- [Schema Versioning](/docs/schema-versioning), how schema changes are versioned and published +- [Tool Authoring](/docs/tool-authoring), how schemas fit into tool changes diff --git a/app/docs/_data/generated/schemas.ts b/app/docs/_data/generated/schemas.ts new file mode 100644 index 0000000..5e2ec11 --- /dev/null +++ b/app/docs/_data/generated/schemas.ts @@ -0,0 +1,41 @@ +export type PublishedSchema = { + name: string + versions: string[] + latest: string +} + +export const publishedSchemas: PublishedSchema[] = [ + { name: "xcodebuildmcp.output.app-path", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.build-result", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.build-run-result", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.build-settings", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.bundle-id", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.capture-result", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.coverage-result", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.debug-breakpoint-result", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.debug-command-result", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.debug-session-action", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.debug-stack-result", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.debug-variables-result", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.device-list", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.doctor-report", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.error", versions: ["1"], latest: "1" }, + { name: "xcodebuildmcp.output.install-result", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.launch-result", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.process-list", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.project-list", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.scaffold-result", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.scheme-list", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.session-defaults", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.session-profile", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.simulator-action-result", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.simulator-list", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.stop-result", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.test-result", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.ui-action-result", versions: ["1", "2", "3"], latest: "3" }, + { name: "xcodebuildmcp.output.workflow-selection", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.xcode-bridge-call-result", versions: ["1", "2", "3"], latest: "3" }, + { name: "xcodebuildmcp.output.xcode-bridge-status", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.xcode-bridge-sync", versions: ["1", "2"], latest: "2" }, + { name: "xcodebuildmcp.output.xcode-bridge-tool-list", versions: ["1", "2", "3"], latest: "3" }, +] diff --git a/app/docs/_data/routes.ts b/app/docs/_data/routes.ts index 7191277..71554a4 100644 --- a/app/docs/_data/routes.ts +++ b/app/docs/_data/routes.ts @@ -11,6 +11,7 @@ export type DocSlug = | "tools" | "mcp-protocol-support" | "output-formats" + | "schemas" | "configuration" | "session-defaults" | "env-vars" @@ -66,6 +67,7 @@ export const PAGES_ORDER: DocSlug[] = [ "workflows", "mcp-protocol-support", "output-formats", + "schemas", "configuration", "session-defaults", "env-vars", @@ -156,6 +158,12 @@ export const PAGE_META: Record = { group: "Reference", description: "Machine-readable CLI output and MCP structuredContent envelopes.", }, + schemas: { + slug: "schemas", + title: "Published Schemas", + group: "Reference", + description: "Browse versioned structured-output JSON Schemas published at stable website URLs.", + }, configuration: { slug: "configuration", title: "Configuration", @@ -330,6 +338,7 @@ export const SIDEBAR_GROUPS: SidebarGroup[] = [ { slug: "workflows" }, { slug: "mcp-protocol-support" }, { slug: "output-formats" }, + { slug: "schemas" }, { slug: "configuration", children: ["session-defaults", "env-vars"], diff --git a/app/docs/_styles/scraps.css b/app/docs/_styles/scraps.css index afd9b92..4339a11 100644 --- a/app/docs/_styles/scraps.css +++ b/app/docs/_styles/scraps.css @@ -1237,6 +1237,27 @@ html[data-docs-theme='dark'] .docs-root .tool-card .tc-badge.beta { color: #ff93ce; } +/* -------- Schema cards -------- */ +.docs-root a.tool-card, +.docs-root a.tool-card:hover { + text-decoration: none; +} +.docs-root .tool-card.schema-card { + grid-template-columns: 1fr auto; +} +.docs-root .tool-card.schema-card > div { + min-width: 0; +} +.docs-root .tool-card.schema-card .tc-name { + overflow-wrap: anywhere; +} +.docs-root .tool-card.schema-card .tc-sub { + margin-top: 2px; + font-family: var(--font-mono); + font-size: 10px; + color: var(--fg-muted); +} + /* -------- Params list -------- */ .docs-root .params { margin: 0 0 20px; @@ -1606,6 +1627,27 @@ html[data-docs-theme='dark'] .docs-root .tool-card .tc-badge.beta { color: var(--fg-primary); outline: none; } +.docs-root .tools-controls .schema-version { + display: inline-flex; + align-items: center; + gap: 8px; +} +.docs-root .tools-controls .schema-version span { + font-size: 12px; + color: var(--fg-muted); +} +.docs-root .tools-controls select { + height: 32px; + padding: 0 10px; + font-family: var(--font-mono); + font-size: 12px; + background: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-radius: 6px; + color: var(--fg-primary); + outline: none; + cursor: pointer; +} .docs-root .tools-controls .seg { flex-wrap: wrap; } diff --git a/app/page.tsx b/app/page.tsx index e3481df..aa28016 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -118,6 +118,12 @@ export default function XcodeBuildMCPLanding() { {item} ))} + + Why XcodeBuildMCP? + ))} + setIsMobileMenuOpen(false)} + className="text-sentry-text-secondary hover:text-white transition-colors" + > + Why XcodeBuildMCP? + setIsMobileMenuOpen(false)} @@ -209,6 +222,13 @@ export default function XcodeBuildMCPLanding() { Get Started + + + Why XcodeBuildMCP? + void; +}) { + return ( + + ); +} + +export function PromptBox({ + currentTimeMs, + prompt, + submitMs, + isReducedMotion = false, +}: { + currentTimeMs: number; + prompt: string; + submitMs: number; + isReducedMotion?: boolean; +}) { + const isSubmitted = isReducedMotion || currentTimeMs >= submitMs; + const typedPrompt = isSubmitted + ? "" + : prompt.slice(0, Math.floor((currentTimeMs / submitMs) * prompt.length)); + + return ( +
+
+ {">"} + + {typedPrompt} + {!isSubmitted ? : null} + +
+
+ ⏵⏵ + {isSubmitted ? "prompt sent" : "press return to start"} + + {isSubmitted ? "(agent continues unattended)" : "(single user action)"} + +
+
+ ); +} + +export function PhaseStepper({ + phases, + activeId, + isComplete, +}: { + phases: AgentPhase[]; + activeId: AgentPhaseId; + isComplete: boolean; +}) { + const activeIndex = Math.max( + 0, + phases.findIndex((phase) => phase.id === activeId), + ); + const active = phases[activeIndex]; + + return ( +
+
+ {phases.map((phase, index) => ( + + ))} +
+
+ {active?.label} + + phase {activeIndex + 1} of {phases.length} + +
+
+ ); +} + +function segmentClass( + index: number, + activeIndex: number, + isComplete: boolean, +) { + if (isComplete || index < activeIndex) return "bg-green-400/70"; + if (index === activeIndex) return "bg-sentry-purple-light"; + return "bg-sentry-dark-600/70"; +} + +export function TerminalRow({ event }: { event: AgentTranscriptEvent }) { + return ( +
+ +
+ ); +} + +function rowSpacingClass(event: AgentTranscriptEvent) { + if (event.kind === "tool-output") return "mt-0.5"; + if (event.kind === "tool-call") return "mt-1"; + return "mt-3 first:mt-0"; +} + +function TerminalRowBody({ event }: { event: AgentTranscriptEvent }) { + switch (event.kind) { + case "prompt": + return ( +
+ {">"} + {event.text} +
+ ); + + case "agent": + return ( +
+ + {event.text} +
+ ); + + case "tool-call": + return ( +
+ + + {event.server ? ( + <> + + {event.server} + + :: + + ) : null} + + {event.tool} + + {event.args ? ( + ({event.args}) + ) : null} + +
+ ); + + case "tool-output": + return ( +
+ {(event.lines ?? []).map((line, index) => ( +
+ + {index === 0 ? "⎿" : " "} + + {line} +
+ ))} + {event.more ? ( +
… {event.more}
+ ) : null} +
+ ); + + case "status": { + const isCompleted = event.status === "completed"; + + return ( +
+ + {isCompleted ? "✓" : "✱"} + + + {event.text} + {isCompleted ? null : "…"} + +
+ ); + } + + case "diff": + return ; + + default: + return null; + } +} + +function DiffHunk({ + file, + diff, +}: { + file?: string; + diff: AgentDiffLine[]; +}) { + return ( +
+ {file ? ( +
+ {file} +
+ ) : null} +
+ {diff.map((line, index) => ( +
+ {diffSign(line.kind)} + {line.text} +
+ ))} +
+
+ ); +} + +function diffLineClass(kind: AgentDiffLine["kind"]) { + if (kind === "added") return "bg-green-500/10 text-green-300"; + if (kind === "removed") return "bg-red-500/10 text-red-300"; + return "text-sentry-text-muted"; +} + +function diffSign(kind: AgentDiffLine["kind"]) { + if (kind === "added") return "+"; + if (kind === "removed") return "-"; + return " "; +} diff --git a/app/why-xcodebuildmcp/agent-demo.tsx b/app/why-xcodebuildmcp/agent-demo.tsx new file mode 100644 index 0000000..d2d6aa6 --- /dev/null +++ b/app/why-xcodebuildmcp/agent-demo.tsx @@ -0,0 +1,460 @@ +"use client"; + +import { Pause, Play, RotateCcw } from "lucide-react"; +import { useEffect, useMemo, useRef, useState, type RefObject } from "react"; +import { + agentDemoPhases, + agentDemoPlaybackRate, + agentDemoPrompt, + agentDemoPromptSubmitMs, + agentDemoVideo, + agentTranscriptEvents, +} from "./agent-demo-data"; +import { + formatTimestamp, + PhaseStepper, + PromptBox, + SpeedBadge, + TerminalRow, +} from "./agent-demo-terminal"; + +const PIN_THRESHOLD_PX = 48; +const PLAYBACK_RATES = [1, 2, 3] as const; +const FINAL_TRANSCRIPT_START_MS = + agentTranscriptEvents[agentTranscriptEvents.length - 1]?.startMs ?? + agentDemoVideo.durationMs; + +function useInView(threshold = 0.6): [RefObject, boolean] { + const ref = useRef(null); + const [inView, setInView] = useState(false); + + useEffect(() => { + const node = ref.current; + + if (!node) { + return; + } + + const observer = new IntersectionObserver( + ([entry]) => setInView(entry.isIntersecting), + { threshold }, + ); + + observer.observe(node); + + return () => observer.disconnect(); + }, [threshold]); + + return [ref, inView]; +} + +export function AgentDemo() { + const [demoRef, isDemoInView] = useInView(0.6); + const videoRef = useRef(null); + const transcriptRef = useRef(null); + const isPinnedRef = useRef(true); + + const [currentTimeMs, setCurrentTimeMs] = useState(0); + const [isReducedMotion, setIsReducedMotion] = useState(false); + const [motionPreferenceKnown, setMotionPreferenceKnown] = useState(false); + const [isPlaying, setIsPlaying] = useState(false); + const [hasVideoError, setHasVideoError] = useState(false); + const [playbackRate, setPlaybackRate] = useState(agentDemoPlaybackRate); + + const visibleEvents = useMemo( + () => + agentTranscriptEvents.filter((event) => { + if (isReducedMotion) return event.status !== "running"; + if (event.status === "running") { + return ( + event.startMs <= currentTimeMs && + currentTimeMs < (event.endMs ?? Number.POSITIVE_INFINITY) + ); + } + + return event.startMs <= currentTimeMs; + }), + [currentTimeMs, isReducedMotion], + ); + + const activePhaseId = + visibleEvents.length > 0 + ? visibleEvents[visibleEvents.length - 1].phase + : agentDemoPhases[0].id; + + const isPhaseStepperComplete = + isReducedMotion || currentTimeMs >= FINAL_TRANSCRIPT_START_MS; + + useEffect(() => { + const media = window.matchMedia("(prefers-reduced-motion: reduce)"); + const updatePreference = () => { + setIsReducedMotion(media.matches); + setMotionPreferenceKnown(true); + }; + + updatePreference(); + media.addEventListener("change", updatePreference); + return () => media.removeEventListener("change", updatePreference); + }, []); + + useEffect(() => { + const video = videoRef.current; + if (!motionPreferenceKnown || !video) return; + + if (isReducedMotion || !isDemoInView) { + video.pause(); + setIsPlaying(false); + return; + } + + void video.play().catch(() => setIsPlaying(false)); + }, [isDemoInView, isReducedMotion, motionPreferenceKnown]); + + useEffect(() => { + const video = videoRef.current; + if (!video) return; + + video.playbackRate = playbackRate; + }, [playbackRate]); + + useEffect(() => { + if (isReducedMotion || !isPinnedRef.current) return; + + const element = transcriptRef.current; + if (!element) return; + + const animationFrame = window.requestAnimationFrame(() => { + element.scrollTop = element.scrollHeight; + }); + + return () => window.cancelAnimationFrame(animationFrame); + }, [isReducedMotion, visibleEvents.length]); + + return ( +
+
+
+
+
+
+
+
+ + + +
+ + claude :: atmos weather + +
+
+ + {formatTimestamp(currentTimeMs)} /{" "} + {formatTimestamp(agentDemoVideo.durationMs)} + + + + +
+
+ +
+ {visibleEvents.map((event) => ( + + ))} +
+ +
+ +
+
+ +
+
+ +
+ setHasVideoError(true)} + onLoadedMetadata={handleVideoLoadedMetadata} + onPause={() => setIsPlaying(false)} + onPlay={() => setIsPlaying(true)} + onSeeked={handleVideoTimeUpdate} + onSeeking={handleVideoTimeUpdate} + onTimeUpdate={handleVideoTimeUpdate} + videoRef={videoRef} + /> +
+
+
+ ); + + function handleVideoLoadedMetadata() { + const video = videoRef.current; + if (!video) return; + + video.playbackRate = playbackRate; + setCurrentTimeMs(Math.round(video.currentTime * 1000)); + } + + function handleVideoTimeUpdate() { + const video = videoRef.current; + if (!video) return; + + setCurrentTimeMs(Math.round(video.currentTime * 1000)); + } + + function handleSpeedCycle() { + setPlaybackRate((currentRate) => { + const currentIndex = PLAYBACK_RATES.findIndex( + (rate) => rate === currentRate, + ); + const nextRate = PLAYBACK_RATES[(currentIndex + 1) % PLAYBACK_RATES.length]; + const video = videoRef.current; + + if (video) video.playbackRate = nextRate; + + return nextRate; + }); + } + + function handlePlaybackToggle() { + const video = videoRef.current; + if (!video) return; + + if (video.paused) { + void video.play().catch(() => setIsPlaying(false)); + return; + } + + video.pause(); + } + + function handleReplay() { + const video = videoRef.current; + if (!video) return; + + video.currentTime = 0; + setCurrentTimeMs(0); + + if (!isReducedMotion) { + void video.play().catch(() => setIsPlaying(false)); + } + } + + function handleTranscriptScroll() { + const element = transcriptRef.current; + if (!element) return; + + const distanceFromBottom = + element.scrollHeight - element.scrollTop - element.clientHeight; + isPinnedRef.current = distanceFromBottom < PIN_THRESHOLD_PX; + } +} + +type PhoneFrameProps = { + autoPlay: boolean; + currentTimeMs: number; + hasVideoError: boolean; + onError: () => void; + onLoadedMetadata: () => void; + onPause: () => void; + onPlay: () => void; + onSeeked: () => void; + onSeeking: () => void; + onTimeUpdate: () => void; + videoRef: RefObject; +}; + +function PhoneFrame({ + autoPlay, + currentTimeMs, + hasVideoError, + onError, + onLoadedMetadata, + onPause, + onPlay, + onSeeked, + onSeeking, + onTimeUpdate, + videoRef, +}: PhoneFrameProps) { + return ( +
+
+
+
+ {!hasVideoError ? ( + + ) : null} +
+
+
+
+ {formatTimestamp(currentTimeMs)} +
+
+ ); +} + +export function AgentDemoStyles() { + return ( + + ); +} diff --git a/app/why-xcodebuildmcp/page.tsx b/app/why-xcodebuildmcp/page.tsx new file mode 100644 index 0000000..02ed81f --- /dev/null +++ b/app/why-xcodebuildmcp/page.tsx @@ -0,0 +1,18 @@ +import type { Metadata } from "next"; +import { WhyXcodeBuildMCPPage } from "./why-xcodebuildmcp-page"; + +export const metadata: Metadata = { + title: "Why XcodeBuildMCP? Agentic Xcode Automation Beyond Bash", + description: + "Why AI agents use XcodeBuildMCP instead of ad-hoc xcodebuild and simctl commands: compact output, session defaults, managed logs, UI automation, LLDB, and deterministic CLI workflows.", + openGraph: { + title: "Why XcodeBuildMCP?", + description: + "See why XcodeBuildMCP gives AI agents a better Xcode feedback loop than raw shell commands.", + url: "https://xcodebuildmcp.com/why-xcodebuildmcp", + }, +}; + +export default function Page() { + return ; +} diff --git a/app/why-xcodebuildmcp/why-xcodebuildmcp-data.ts b/app/why-xcodebuildmcp/why-xcodebuildmcp-data.ts new file mode 100644 index 0000000..457ceef --- /dev/null +++ b/app/why-xcodebuildmcp/why-xcodebuildmcp-data.ts @@ -0,0 +1,103 @@ +import type { LucideIcon } from "lucide-react"; +import { + Bug, + FileText, + Gauge, + Layers, + MonitorSmartphone, + Settings2, +} from "lucide-react"; + +export const appleToolchain = [ + "xcodebuild", + "simctl", + "devicectl", + "log", + "lldb", + "Simulator UI", + "debugger surfaces", +]; + +export const bashLines = [ + "$ xcodebuild -workspace App.xcworkspace -scheme App -destination 'platform=iOS Simulator,name=iPhone 17 Pro' build", + "... pages of compiler, linker, package, and warning output ...", + "$ xcrun simctl boot 7D8B...", + "$ xcrun simctl install 7D8B... DerivedData/.../App.app", + "$ xcrun simctl launch 7D8B... io.example.App", + "$ xcrun simctl spawn 7D8B... log stream --predicate ...", +]; + +export const mcpSteps = [ + "Remember scheme, workspace, simulator, and device through session defaults instead of repeating flags", + "Run build, install, launch, and log capture as workflow tool calls, not fragile multi-command loops", + "Return schema-backed structured results instead of raw terminal transcript noise", + "Keep logs, screenshots, videos, and build artifacts available without pasting them into chat", + "Expose semantic UI snapshots, UI actions, and stateful LLDB sessions when the fix needs evidence", +]; + +export const tokenComparison = { + bash: { + label: "Raw shell loop", + value: "High noise", + }, + mcp: { + label: "MCP tool result", + value: "Structured", + }, + summary: "schema-backed result plus artifact links", +}; + +export const benefits: Array<{ + icon: LucideIcon; + title: string; + text: string; +}> = [ + { + icon: Gauge, + title: "Less output, more signal", + text: "Raw Xcode output is a transcript. XcodeBuildMCP turns it into status, errors, warnings, artifacts, and next steps the agent can act on.", + }, + { + icon: Settings2, + title: "Project memory for the agent", + text: "Set workspace, scheme, simulator, device, and profile once. Later calls stay short, so the agent focuses on the bug instead of retyping flags.", + }, + { + icon: FileText, + title: "Published structured contracts", + text: "Tool results are schema-backed and versioned, so agents and scripts can read stable JSON fields instead of scraping rendered text.", + }, + { + icon: MonitorSmartphone, + title: "Simulator UI as a first-class surface", + text: "Agents can inspect semantic UI snapshots, tap, type, swipe, drag, press keys, capture screenshots, and record video with stable element references.", + }, + { + icon: Bug, + title: "Debugger control that survives turns", + text: "LLDB attach, breakpoints, stack frames, variables, and raw commands are exposed as tools backed by stateful debug sessions.", + }, + { + icon: Layers, + title: "Artifacts are managed, not pasted", + text: "Build logs, runtime logs, screenshots, videos, and app paths are captured as artifacts the agent can inspect without flooding every response.", + }, +]; + +export const stories = [ + { + role: "Developer", + title: "You stay in review mode", + text: "Ask the agent to build, install, launch, inspect the UI, capture logs, debug, fix, validate, and return proof. You no longer become the build operator between every turn.", + }, + { + role: "Agent", + title: "Apple tools stay the source of truth", + text: "XcodeBuildMCP does not replace Xcode or xcodebuild. It orchestrates Apple's own xcodebuild, simctl, devicectl, log, lldb, simulator, and debugger surfaces through a narrower contract.", + }, + { + role: "When raw xcodebuild is enough", + title: "Stable CI can stay simple", + text: "If you already have a stable archive or CI script, raw xcodebuild may be the right tool. XcodeBuildMCP earns its place in iterative agent-assisted workflows where state, UI, logs, and debugging all matter.", + }, +]; diff --git a/app/why-xcodebuildmcp/why-xcodebuildmcp-page.tsx b/app/why-xcodebuildmcp/why-xcodebuildmcp-page.tsx new file mode 100644 index 0000000..4e66a0f --- /dev/null +++ b/app/why-xcodebuildmcp/why-xcodebuildmcp-page.tsx @@ -0,0 +1,434 @@ +"use client"; + +import { useState } from "react"; +import { + CheckCircle, + ChevronRight, + Code2, + Eye, + Github, + Menu, + X, + XCircle, +} from "lucide-react"; +import Image from "next/image"; +import Link from "next/link"; +import { AgentDemo, AgentDemoStyles } from "./agent-demo"; +import { + appleToolchain, + bashLines, + benefits, + mcpSteps, + stories, + tokenComparison, +} from "./why-xcodebuildmcp-data"; + +type TokenBadgeProps = { + label: string; + value: string; + tone: "bash" | "mcp"; + className?: string; + delta?: string; +}; + +function TokenBadge({ + label, + value, + tone, + className = "", + delta, +}: TokenBadgeProps) { + const toneClassName = + tone === "bash" + ? "border-red-500/30 ring-red-500/10" + : "border-green-400/30 ring-green-400/10"; + const accentClassName = tone === "bash" ? "bg-red-400" : "bg-green-400"; + + return ( +
+ +
+ + {label} + + + {value} + +
+ {delta ? ( + + {delta} + + ) : null} +
+ ); +} + +export function WhyXcodeBuildMCPPage() { + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + + return ( +
+
+
+
+ + XcodeBuildMCP + + XcodeBuildMCP + + + + + +
+ + + GitHub + + + Get Started + + +
+
+ + {isMobileMenuOpen ? ( +
+ +
+ ) : null} +
+
+ +
+
+
+
+
+

+ Stop feeding agents raw Xcode output. + + Give them the simulator. + +

+

+ XcodeBuildMCP does not replace Xcode or xcodebuild. It + orchestrates Apple's own tools into a closed loop: chat + request, tool call, simulator interaction, managed logs, + debugger state, and proof the agent can explain back to you + without dumping raw build output into the conversation. +

+
+ {appleToolchain.map((tool) => ( + + {tool} + + ))} +
+
+ +
+
+ +
+
+
+

+ Agents need more than shell access. +

+

+ Agentic iOS work is not one build command. It is build, + install, launch, inspect UI, capture logs, debug, fix, and + validate. A shell can run commands, but it does not remember the + project, reduce noise, manage debug state, or turn UI into + something an agent can reason about. +

+
+
+
+ +
+ +

Ad-hoc shell loop

+
+
+ {bashLines.map((line) => ( +
+ {line} +
+ ))} +
+
+
+ +
+ +

+ Agentic tool contract +

+
+
+ {mcpSteps.map((step) => ( +
+ + {step} +
+ ))} +
+
+
+

+ In this illustrative loop, XcodeBuildMCP returns a{" "} + + {tokenComparison.summary} + {" "} + instead of asking the agent to parse every transcript line. +

+
+
+ +
+
+
+

+ Why agents do better with XcodeBuildMCP +

+

+ It breaks the Xcode IDE bottleneck while keeping Apple's + toolchain as the source of truth. Results are schema-backed and + versioned; see the{" "} + + published schema browser + . +

+
+
+ {benefits.map((benefit) => ( +
+
+ +
+

{benefit.title}

+

+ {benefit.text} +

+
+ ))} +
+
+
+ +
+
+ {stories.map((story) => ( +
+

+ {story.role} +

+

{story.title}

+

+ {story.text} +

+
+ ))} +
+
+ +
+
+
+ +
+

Give the agent hands and eyes.

+

+ Use MCP for live collaboration. Use the CLI for deterministic + scripts. Either way, your agent gets a purpose-built Xcode + interface instead of a pile of terminal output. +

+
+ + Start with setup + + + See demos + +
+
+
+ +
+

+ Demo workflow is illustrative, not a benchmark. It is + representative of a real agent-assisted session using XcodeBuildMCP, + but it has been simplified for clarity. Output volume and exact tool + behavior vary by task, project, build settings, and agent client. +

+
+
+ +
+
+
+
+ XcodeBuildMCP + XcodeBuildMCP + © {new Date().getFullYear()} Sentry +
+ +
+ + Sentry + + + + + + @xcodebuildmcp + + + + GitHub + +
+
+
+
+ + +
+ ); +} diff --git a/public/videos/why-xcodebuildmcp/atmos-weather-agent-demo-poster.jpg b/public/videos/why-xcodebuildmcp/atmos-weather-agent-demo-poster.jpg new file mode 100644 index 0000000..9ae0a21 Binary files /dev/null and b/public/videos/why-xcodebuildmcp/atmos-weather-agent-demo-poster.jpg differ diff --git a/public/videos/why-xcodebuildmcp/atmos-weather-agent-demo.mp4 b/public/videos/why-xcodebuildmcp/atmos-weather-agent-demo.mp4 new file mode 100644 index 0000000..e3ea987 Binary files /dev/null and b/public/videos/why-xcodebuildmcp/atmos-weather-agent-demo.mp4 differ