Skip to content
Open
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
283 changes: 283 additions & 0 deletions Docs/xcode-27-headless-mcp-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
# Xcode 27 Headless MCP Design

## Status

- Design owner: `codex/xcode-27-headless-mcp`
- Baseline: `b251cecfa059ce7b5f0a9c9a8b7f9480e390edb3`
- Verified Xcode: 27.0 build `27A5252f`
- Verified Xcode MCP server: `xcode-tools` version `25295.11`
- Implementation: complete; automated validation complete
- Live workspace validation: pending manual Xcode Service approval

This document is the design contract and progress ledger for Xcode 27 headless
MCP support. Update it before changing a public API or owner boundary.

## Consumer stories

### Default CLI

```sh
xcode-mcp-proxy-server --auto-approve
```

The server uses Xcode 27's headless MCP service when the selected Xcode ships
`mcp-server` and headless access is enabled. Otherwise it preserves GUI Xcode
routing. When headless access is available but disabled, startup emits one
actionable notice and continues with GUI routing.

### Explicit selection

```sh
xcode-mcp-proxy-server --xcode-mode gui
xcode-mcp-proxy-server --xcode-mode headless
```

Explicit GUI mode never consults or launches the headless service. Explicit
headless mode fails startup when the selected Xcode does not ship `mcp-server`
or headless access is disabled. It never silently falls back to GUI routing.

### Embedded server

```swift
let server = XcodeMCPProxyServer(
configuration: .init(xcodeMode: .automatic)
)
let endpoint = try await server.start()
defer { Task { try? await server.shutdown() } }
```

`xcodeMode` is a connection-selection policy. The existing `Upstream` value
continues to own the bridge command, arguments, pool size, and optional MCP
session identifier.

## Public interface sketch

```swift
public struct XcodeMCPProxyServerConfiguration: Equatable, Sendable {
public enum XcodeMode: String, Equatable, Sendable {
case automatic
case gui
case headless
}

public var xcodeMode: XcodeMode

public init(
bindAddress: BindAddress = .localhost(),
upstream: Upstream = .defaultMCPBridge(),
maxBodyBytes: Int = 1_048_576,
requestTimeout: Duration? = .seconds(300),
configurationFileURL: URL? = nil,
toolPolicy: ToolPolicy? = nil,
initializeHandshake: InitializeHandshake? = nil,
discovery: Discovery = .defaultLocation,
approvalPolicy: ApprovalPolicy = .manual,
featurePolicy: FeaturePolicy = .default,
xcodeMode: XcodeMode = .automatic
)
}
```

Adding `xcodeMode` with a default preserves existing source call sites. The new
enum is a closed consumer choice and is therefore intentionally exhaustive.

## Verified upstream contract

The following facts were observed with `MCP_XCODE_PID` absent and the selected
Xcode 27 developer directory in `DEVELOPER_DIR`:

1. `xcrun mcp-server status --format json` exits successfully while disabled
and returns `permission.enabled`, `permission.unsafeAlwaysAllowAllAgents`,
`running`, and `openWorkspaces`.
2. `openWorkspaces` is not a stable scalar shape: when populated it is an array
of objects containing `path`, `displayName`, and `activeSchemeName`.
Availability resolution therefore decodes only `permission.enabled` and
preserves or ignores unknown fields.
3. An unbound `mcpbridge` initializes successfully against headless
`XcodeService` and returns a 54-tool catalog.
4. The headless catalog owns workspace lifecycle through
`XcodeOpenWorkspace`, `XcodeListWorkspaces`, and `XcodeCloseWorkspace`.
XcodeMCPKit must not duplicate that state or require a workspace path at
proxy startup.
5. First use of `XcodeOpenWorkspace` is the approval boundary for both agent
identity and the containing folder. Before approval, catalog discovery is
available while workspace tools return an actionable tool error.
6. `mcp-server open <path>` is service administration, not the agent approval
boundary. It must not be used as a substitute for `XcodeOpenWorkspace`.
7. Xcode Service is process-shared. XcodeMCPKit owns its child `mcpbridge`
processes, but does not own or stop Xcode Service.
8. With headless access enabled and Xcode Service stopped, launching an unbound
`mcpbridge` starts Xcode Service and completes `initialize`. XcodeMCPKit does
not need to call `mcp-server start`.

The preview CLI may return valid status JSON together with a nonzero status or
warning when its live service query times out. A valid JSON payload is the
status fact; stderr and exit status remain diagnostics.

## Mode resolution

Mode resolution happens once during server start, before the runtime and HTTP
gateway acquire resources.

| Requested mode | Stock `mcpbridge` | `mcp-server` state | Effective mode |
| --- | --- | --- | --- |
| automatic | yes | installed and enabled | headless |
| automatic | yes | installed and disabled | GUI + notice |
| automatic | yes | not installed | GUI |
| automatic | yes | status unavailable or malformed | GUI + warning |
| gui | yes | any | GUI; status is not queried |
| headless | yes | installed and enabled | headless |
| headless | yes | disabled, unavailable, or malformed | startup error |
| automatic | custom upstream | not applicable | existing custom unbound mode |
| gui/headless | custom upstream | not applicable | configuration error |

The disabled notice is one multiline log event:

```text
Xcode 27 headless MCP is available but disabled.

To enable it, run:

sudo xcrun mcp-server enable

XcodeMCPKit will continue using GUI Xcode routing.
```

XcodeMCPKit never executes `enable`, `approve`, `allow-folder`, `deny`,
`clear-permissions`, or an unsafe permission command.

## Owner map

| Responsibility | Owner |
| --- | --- |
| Requested GUI/headless/automatic policy | `XcodeMCPProxyServerConfiguration` / `ProxyConfig` |
| `mcp-server` discovery, status execution, and narrow JSON decoding | new internal status client in `XcodeMCPProxyKit` |
| Effective mode selection and user-facing notice/error | server lifecycle acquisition |
| GUI Xcode process inventory | existing `XcodeProcessEventMonitor` |
| GUI process-bound bridge membership and catalogs | existing `ProcessControlPlaneAuthority` |
| Headless bridge process and catalog | existing unbound `MCPBridgeRuntime` path |
| Headless workspace membership and identifiers | upstream Xcode Service tools |
| GUI window/tab identity | existing `WindowOwnershipAuthority` |
| Device interaction token affinity | new runtime affinity authority |
| Downstream HTTP session and progress-token ownership | existing session and lease authorities |

No new package, product, or target is required. The new external-I/O adapter is
an internal `XcodeMCPProxyKit` responsibility; the runtime receives only the
resolved mode.

## Runtime and lifecycle contract

- GUI mode preserves process-bound discovery, `MCP_XCODE_PID`, per-Xcode pools,
AX permission automation, and the proxy DocumentationSearch provider.
- Headless mode launches the configured stock bridge without
`MCP_XCODE_PID`. It does not wait for a GUI Xcode process and does not run AX
permission automation.
- Headless mode forwards the upstream DocumentationSearch and workspace tools;
it does not create a second workspace or documentation source of truth.
- Proxy shutdown closes and awaits its bridge/runtime/HTTP resources. It does
not call `mcp-server stop`.
- Status resolution is part of startup acquisition. Cancellation of startup
cancels and awaits the status process through `ProcessRunner`.
- A disabled headless service is a normal automatic-mode candidate result. A
malformed response or execution failure is diagnostic, not silently
equivalent to disabled.

## Tool-surface compatibility

The Xcode tool catalog remains dynamic. Do not add one Swift method per Xcode
tool. Headless-specific tools and future catalog fields pass through unchanged.

The proxy-owned `XcodeRefreshCodeIssuesInFile` workflow must be checked against
the headless schema before it is enabled in headless mode. If the headless tool
uses `workspaceIdentifier` rather than the GUI tab contract, the effective
headless configuration forwards this tool upstream instead of guessing a GUI
owner.

## Device interaction affinity

`DeviceInteractionStartSession` and
`DeviceInteractionStartWorkspaceSession` return `interactionSessionKey`.
Follow-up tools use two spellings:

- `DeviceInteractionSynthesize`: `interactSessionKey`
- `DeviceInteractionInstallAndRun` and `DeviceInteractionEndSession`:
`interactionSessionKey`

For every upstream topology, the runtime records the returned key together with
the exact upstream topology proof that created it. Routed GUI pools additionally
record the stable process-route identity needed for window admission and
identifier rewriting. Follow-up requests are admitted only to the recorded
upstream proof. Route replacement, retirement, session end, and runtime shutdown
evict the corresponding affinity. An unknown key follows the upstream's ordinary
error path only when a single unbound upstream exists; it is never guessed across
multiple process-routed or unbound upstreams.

The affinity authority owns token membership. Request routing consumes an
immutable snapshot/proof and revalidates it before send. It does not mirror
device state or own the device-session lifecycle itself.

## Progress and verifier contract

- Existing progress-token rewriting and per-operation delivery remain the
single source of truth.
- The live verifier records progress notifications for build/test operations
and preserves their raw fields in its report.
- The verifier gains a headless path that calls `XcodeOpenWorkspace`, uses the
returned `workspaceIdentifier`, and always calls `XcodeCloseWorkspace` for a
workspace it opened.
- Live verification remains opt-in and never enables or broadly approves
headless access.

## Signing decision

The current release artifact is ad-hoc signed. Xcode Service identified the
probe's actual host executable as the agent identity, not `mcpbridge`. After a
release-shaped XcodeMCPKit binary connects headlessly, inspect the recorded
identity and approval duration. Developer ID signing and notarization are a
follow-up only if durable trust rejects the artifact or fails to survive an
upgrade. No signing credential or workflow change is part of this design until
that behavior is observed.

## Failure semantics

| Boundary | Behavior |
| --- | --- |
| `mcp-server` absent in automatic mode | use GUI routing |
| headless disabled in automatic mode | emit notice once; use GUI routing |
| status command fails or JSON is malformed in automatic mode | emit warning; use GUI routing |
| explicit headless unavailable or disabled | fail startup with actionable configuration error |
| agent/folder approval pending | preserve upstream tool error; do not auto-approve or retry-loop |
| headless service exits after connection | existing upstream health/recovery semantics apply |
| proxy shuts down | stop owned bridges; leave shared Xcode Service running |

## Validation

- Status-client unit tests: unavailable, disabled, enabled, populated dynamic
`openWorkspaces`, valid JSON with nonzero exit, malformed JSON, timeout, and
cancellation.
- CLI/config tests for all modes and custom-upstream conflicts.
- Runtime tests proving GUI mode remains process-bound and headless mode is
unbound with no GUI readiness launch.
- Startup-summary and exact multiline notice tests.
- Public product contract compile test for `xcodeMode`.
- Device-affinity owner and routing tests, including both key spellings,
process-routed and unbound pools, replacement, retirement, end, and unknown
keys.
- Existing fast, process, adapter, and full maintainer checks.
- Opt-in live headless initialize, catalog, workspace open/list/close, progress,
and shutdown verification against Xcode 27.

## Progress ledger

- [x] Create task branch and record baseline.
- [x] Verify status JSON while disabled and enabled.
- [x] Verify headless initialize and 54-tool catalog.
- [x] Verify workspace tools are the approval/bootstrap boundary.
- [x] Verify unbound `mcpbridge` starts Xcode Service on demand.
- [x] Implement mode/status resolution and notice.
- [x] Implement resolved runtime ownership and public/CLI surface.
- [x] Implement device interaction affinity.
- [x] Extend verifier and documentation.
- [x] Run automated validation and clean `codex-review`.
- [ ] Complete post-approval live workspace open/list/close and progress
verification.
94 changes: 88 additions & 6 deletions Docs/xcode-27-mcpbridge-tools.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,99 @@
# Xcode 27 mcpbridge Tool Additions
# Xcode 27 mcpbridge Tool Surfaces

## Verified Environment

- Xcode 26.6: `/Applications/Xcode.app`, build `17F109`, `xcode-tools` server version `24950`
- Xcode 27.0: `/Applications/Xcode_27.app`, build `27A5209h`, `xcode-tools` server version `25245.3`
- Xcode 27.0 GUI baseline: `/Applications/Xcode_27.app`, build `27A5209h`, `xcode-tools` server version `25245.3`
- Xcode 27.0 headless: `/Applications/Xcode_27.app`, build `27A5252f`, `xcode-tools` server version `25295.11`
- MCP protocol: `2025-06-18`

Xcode 26.6's `tools/list` returned 21 tools. Xcode 27.0's `tools/list`
returned 43 tools. No tools were removed; 22 tools were added.
Xcode 26.6's `tools/list` returned 21 tools. The Xcode 27 GUI baseline returned
43 tools: no Xcode 26.6 tool was removed and 22 tools were added. The newer
Xcode 27 headless server returned 54 tools. Its catalog is not a strict
superset of the GUI catalog: it replaces GUI window ownership with workspace
lifecycle tools and omits two other GUI-only queries.

This document is based on the raw `tools/list` descriptors, the `mcp__xcode`
tool metadata exposed to Codex, and light read-oriented runtime checks against
the Xcode 27 MCP server.

## Added Tools
## Headless Workspace Surface

With `MCP_XCODE_PID` absent, an unbound `mcpbridge` connected to Xcode Service
and advertised 54 tools. Relative to the 43-tool GUI baseline, the headless
catalog adds these 14 tools:

| Category | Tool | Primary purpose |
| --- | --- | --- |
| Workspace lifecycle | `XcodeOpenWorkspace` | Open a project or workspace for this agent and return its identifier |
| Workspace lifecycle | `XcodeListWorkspaces` | List workspaces currently available through Xcode Service |
| Workspace lifecycle | `XcodeCloseWorkspace` | Close one workspace by identifier |
| Target inspection | `XcodeListTargets` | List targets in the selected workspace |
| Target inspection | `GetTargetBuildSettings` | Read target build settings |
| Target mutation | `UpdateTargetBuildSetting` | Change one target build setting |
| Target mutation | `AddEntitlement` | Add an entitlement through Xcode's project model |
| Target mutation | `AddInfoPlist` | Add or update an Info.plist entry through Xcode's project model |
| Project creation | `XcodeListTemplates` | List templates available for project or target creation |
| Project creation | `XcodeNewProject` | Create a project from a template |
| Project creation | `XcodeNewTarget` | Add a target from a template |
| Test plans | `XcodeListTestPlans` | List test plans in the selected workspace |
| Test plans | `XcodeSwitchTestPlan` | Change the active test plan |
| Device interaction | `DeviceInteractionStartWorkspaceSession` | Start device interaction from a headless workspace |

The headless catalog does not advertise `XcodeListWindows`,
`XcodeGetCurrentFile`, or `XcodeListNavigatorIssues`. These depend on GUI
window, editor, or navigator state. Consumers must detect
`XcodeListWorkspaces` versus `XcodeListWindows` from `tools/list`; Xcode version
strings are not a reliable routing contract.

### Workspace lifecycle and approval bootstrap

The verified headless lifecycle is:

1. Call `tools/list`; catalog discovery does not require an open workspace.
2. Call `XcodeOpenWorkspace` with required `path`.
3. Keep the returned `workspaceIdentifier` and optional `workspacePath`.
4. Pass that identifier to workspace-scoped tools when their current schema
advertises `workspaceIdentifier`.
5. Call `XcodeCloseWorkspace` with the same identifier only if this client
opened the workspace.

`XcodeOpenWorkspace` is also the first-use approval boundary for the agent
identity and containing folder. Before approval, `XcodeListWorkspaces` and
other workspace calls can return an actionable tool error directing the caller
to open or create a workspace. `xcrun mcp-server open <path>` administers the
shared service but does not replace this approval bootstrap.

XcodeMCPKit never enables headless access, approves an agent or folder, broadens
permission policy, or stops Xcode Service. The service is process-shared; a
client owns only its `mcpbridge` child and workspace handles it explicitly
acquired.

### Schema differences

- GUI workspace tools use `tabIdentifier`; headless workspace tools advertise
optional `workspaceIdentifier`, described as either the identifier returned
by `XcodeOpenWorkspace` or an absolute workspace path.
- `XcodeOpenWorkspace` requires `path` and returns required
`workspaceIdentifier` plus optional `workspacePath`, `activeScheme`,
`activeRunDestination`, and `message` fields.
- `XcodeListWorkspaces` takes no arguments.
- `XcodeCloseWorkspace` requires `workspaceIdentifier`.
- `DeviceInteractionStartWorkspaceSession` requires `sessionIdentifier`,
accepts optional `workspaceIdentifier`, and returns
`interactionSessionKey`.
- Follow-up device calls retain two key spellings:
`DeviceInteractionSynthesize` requires `interactSessionKey`, while
`DeviceInteractionInstallAndRun` and `DeviceInteractionEndSession` require
`interactionSessionKey`.

The live verifier writes the full raw headless catalog to
`ProxyToolVerifierOutput/headless-tool-catalog.json`. It validates a planned
call's arguments against that run's input schema, reports unknown or unsafe
tools as `not-planned`, and preserves raw `MCPProgress` payloads for build and
test operations in `report.json`.

## Xcode 27 GUI Additions over Xcode 26.6

| Category | Tool | Primary purpose |
| --- | --- | --- |
Expand Down Expand Up @@ -336,7 +416,9 @@ valid version and signature.

## Practical Notes

- Most added tools require `tabIdentifier`. Start with `XcodeListWindows` to select the target workspace tab.
- In the GUI catalog, most tools require `tabIdentifier`; start with
`XcodeListWindows`. In the headless catalog, open the workspace and use the
returned `workspaceIdentifier` only where the advertised schema accepts it.
- `XcodeSwitchScheme` and `XcodeSwitchRunDestination` change Xcode UI state. Use list tools for inspection-only workflows.
- `RunProject`, `DeviceInteraction*`, `StringCatalogEdit`, and `UpdateFileCompilerFlags` change project, device, or file state. Prefer fixtures or scratch projects when validating them.
- Organizer diagnostics may succeed at the MCP transport level while returning structured `success=false` for product, platform, App Store Connect, or Organizer availability issues. Treat this separately from an MCP error.
Expand Down
Loading