Skip to content
Closed
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
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,28 @@

## [Unreleased]

### Added

- Added experimental opt-in Simulator operation leases with isolated storage, persistent worktree bindings, MCP/CLI/daemon management, and target enforcement for boot readiness, installation, app launch, screenshots, UI snapshots, taps, and text input. Explicit provisioning creates and verifies a dedicated device once per worktree, retaining uncertain creation records instead of automatically replacing devices. Managed boot holds the operation until readiness is verified and blocks handoff on uncertain completion. Managed app launch returns its PID without background log capture and retains uncertain launch outcomes. Managed UI references expire across refreshes, handoffs, and intervening runtime calls; uncertain commands prevent follow-up actions. Unsupported tools, including batch UI actions and background capture, remain blocked in managed mode.
- Added managed compile-only build support for `build_sim` under experimental Simulator operation leases, requiring explicit source inputs, scheme, and operation credentials. Managed builds run standard supervised `xcodebuild` on the bound simulator, invalidate prior UI snapshots, require explicit release on success or compile failure, and reject extra arguments, test preparation, or incremental build overrides.
- Added managed two-phase `test_sim` support under experimental Simulator operation leases, with explicit source and operation credentials, serialized supervised test execution, asynchronous result metadata, dedicated Simulator shutdown verification, and explicit cleanup semantics.

### Changed

- Dictionary-shaped MCP inputs now use client-compatible wire representations ([#491](https://github.com/getsentry/XcodeBuildMCP/issues/491)). The `env` and `testRunnerEnv` inputs on build, launch, test, and session-default tools are arrays of `{ "key": "...", "value": "..." }` entries, while `xcode_ide_call_tool.arguments` is a JSON object string. XcodeBuildMCP converts these values to their existing internal objects only after MCP input validation.

### Fixed

- Fixed UI automation reporting a missing Simulator when its UUID contains lowercase letters, including UUIDs returned by managed operation leases.
- Fixed xcodebuild reporting "Unable to find device" when simulatorId contains lowercase letters by normalizing the destination UUID to uppercase at the xcodebuild command boundary.
- Fixed managed test execution rejecting Xcode's invalid `-test-iterations 1` flag by preserving project and test-plan repetition settings while retaining serialized simulator destinations.
- Fixed managed Phase 1 test failures with confirmed numeric nonzero exits being misclassified as uncertain; Phase 2 continues to accept only exits 0 and 65 as known outcomes.
- Fixed managed build and test admission to require exactly one explicit nonblank project or workspace input together with a nonblank scheme and bound Simulator UUID.
- Fixed daemon socket resolution so an explicit `XCODEBUILDMCP_SOCKET` is honored before ordinary workspace discovery, while managed mode continues to validate the isolated workspace and resource namespace.
- Fixed managed activity completion to persist retry and replay receipts before compacting the operation header, and to wait through lock contention instead of abandoning finalization at the normal 10-second transaction deadline.
- Fixed `resource_operation` end and cancel calls from reporting success without changing a blocked operation; blocked leases now return an explicit no-recovery error.
- Fixed managed `test_sim` to allow safe `-only-testing` and `-skip-testing` selectors while continuing to reject destination and execution-policy overrides.

## [2.7.0]

### New! Xcode 27 Device Hub simulator support
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ For further information on installing skills, see [Agent Skills](https://xcodebu
- XcodeBuildMCP requests xcodebuild to skip macro validation to avoid errors when building projects that use Swift Macros.
- Device tools require code signing to be configured in Xcode. See [Device Code Signing](https://xcodebuildmcp.com/docs/device-signing).

## Experimental managed Simulator operations

Managed Simulator provisioning and worktree-scoped operation leases are experimental and opt-in. See the [managed Simulator operations guide](src/resource-management/README.md) for configuration, lease lifecycle, tool contracts, and limitations.

## Privacy

XcodeBuildMCP uses Sentry for internal runtime error telemetry only. For details and opt-out instructions, see [Privacy & Telemetry](https://xcodebuildmcp.com/docs/privacy).
Expand Down
4 changes: 2 additions & 2 deletions manifests/tools/boot_sim.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ module: mcp/tools/simulator/boot_sim
names:
mcp: boot_sim
cli: boot
description: Boot iOS simulator for manual/non-build flows. Not required before simulator build-and-run (build_run_sim).
description: Boot iOS simulator for manual/non-build flows. Not required before simulator build-and-run (build_run_sim). In managed mode, requires an operation lease and waits for boot readiness.
outputSchema:
schema: xcodebuildmcp.output.simulator-action-result
version: "2"
version: '2'
annotations:
title: Boot Simulator
readOnlyHint: false
Expand Down
2 changes: 1 addition & 1 deletion manifests/tools/build_sim.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module: mcp/tools/simulator/build_sim
names:
mcp: build_sim
cli: build
description: Build for iOS sim (compile-only, no launch).
description: Build for iOS sim (compile-only, no launch). In managed mode, executes standard supervised xcodebuild under an operation lease and requires explicit projectPath/workspacePath, scheme, simulatorId, and operation credentials. Non-empty extraArgs, buildForTesting=true, testProductsPath, and preferXcodebuild=false are rejected. Operation remains held after successful builds and confirmed compile failures until explicit end; uncertain command completion or open streams block the lease. Supervision covers direct child exit and closed streams, without process-tree guarantees for user build scripts. build_run_sim remains unsupported in managed mode.
outputSchema:
schema: xcodebuildmcp.output.build-result
version: '3'
Expand Down
4 changes: 2 additions & 2 deletions manifests/tools/launch_app_sim.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ module: mcp/tools/simulator/launch_app_sim
names:
mcp: launch_app_sim
cli: launch-app
description: Launch app on simulator. Runtime logs are captured automatically and the log file path is included in the response.
description: Launch app on simulator. In managed mode, requires an operation lease and explicit bundle ID, restarts the app, and returns its PID without background log capture. Otherwise, runtime logs are captured automatically and the log file path is included in the response.
outputSchema:
schema: xcodebuildmcp.output.launch-result
version: "2"
version: '2'
annotations:
title: Launch App Simulator
readOnlyHint: false
Expand Down
16 changes: 16 additions & 0 deletions manifests/tools/resource_operation.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
id: resource_operation
module: mcp/tools/resource-management/resource_operation
names:
mcp: resource_operation
cli: operation
description: Begin, inspect, poll, end, or cancel an operation on this worktree's bound Simulator. Retain the returned credentials across calls; explicitly end after all work is finished. Does not provision a Simulator.
predicates:
- managedResourcesEnabled
outputSchema:
schema: xcodebuildmcp.output.resource-operation
version: '1'
annotations:
title: Manage Simulator Operation
readOnlyHint: false
destructiveHint: false
openWorldHint: false
16 changes: 16 additions & 0 deletions manifests/tools/resource_provision.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
id: resource_provision
module: mcp/tools/resource-management/resource_provision
names:
mcp: resource_provision
cli: provision
description: Create this worktree's dedicated Simulator on first use, or verify its existing binding. Requires explicit device type and runtime identifiers. Does not boot, replace, delete, or adopt an existing device. Begin an operation separately before using the Simulator.
predicates:
- managedResourcesEnabled
outputSchema:
schema: xcodebuildmcp.output.resource-binding
version: '1'
annotations:
title: Provision Worktree Simulator
readOnlyHint: false
destructiveHint: false
openWorldHint: false
2 changes: 1 addition & 1 deletion manifests/tools/snapshot_ui.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module: mcp/tools/ui-automation/snapshot_ui
names:
mcp: snapshot_ui
cli: snapshot-ui
description: Capture a semantic rs/1 runtime UI snapshot with elementRef targets. Observe once, use tap for one target or batch for multiple same-screen targets, and refresh after navigation, scrolling, sheet changes, or obvious layout changes.
description: Capture a semantic rs/1 runtime UI snapshot with elementRef targets. In managed mode, requires operation credentials and always returns a full snapshot with fresh refs; batch and wait_for_ui are not yet supported. Observe once, use tap for one target or batch for multiple same-screen targets, and refresh after navigation, scrolling, sheet changes, or obvious layout changes.
outputSchema:
schema: xcodebuildmcp.output.capture-result
version: '2'
Expand Down
2 changes: 1 addition & 1 deletion manifests/tools/tap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ names:
mcp: tap
cli: tap
description: >-
Tap one elementRef from the latest snapshot_ui or wait_for_ui output. The elementRef must list the tap action in the snapshot targets; do not use refs from text-only rows. For multiple same-screen taps or visible switch toggles with no intermediate assertion, use batch instead of repeated tap calls. Other same-screen refs may remain usable after success; refresh after navigation, scrolling, sheet changes, or obvious layout changes.
Tap one elementRef from the latest snapshot_ui or wait_for_ui output. In managed mode, requires operation credentials and a ref from this operation and runtime; use fresh refs returned after each action, and batch is not yet supported. The elementRef must list the tap action in the snapshot targets; do not use refs from text-only rows. For multiple same-screen taps or visible switch toggles with no intermediate assertion, use batch instead of repeated tap calls. Other same-screen refs may remain usable after success; refresh after navigation, scrolling, sheet changes, or obvious layout changes.
outputSchema:
schema: xcodebuildmcp.output.ui-action-result
version: '2'
Expand Down
4 changes: 2 additions & 2 deletions manifests/tools/test_sim.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ module: mcp/tools/simulator/test_sim
names:
mcp: test_sim
cli: test
description: Test on iOS sim.
description: Test on iOS sim. In managed mode, executes standard supervised two-phase xcodebuild under an operation lease and requires explicit projectPath or workspacePath, scheme, simulatorId, and operation credentials. Prepared test artifacts, unsafe extraArgs, simulatorName, buildForTesting, and preferXcodebuild=false are rejected; -only-testing and -skip-testing selectors are allowed. testRunnerEnv is accepted in wire format. Phase 2 disables parallel testing and limits concurrent simulator destinations to one while preserving project and test-plan repetition settings. On normal phase 2 exit 0 or 65, metadata extraction is followed by shutdown of the specified Simulator and a unique available-catalog verification of its Shutdown state, clearing running app state without erase/delete or auto-reboot (subsequent UI automation requires boot_sim). Confirmed phase 1 compile failures do not shut down the simulator. Completed runs release upon explicit end; in-flight calls hold concurrent end in closing. Signals, rejected or unknown commands, open streams, cleanup failures, or unexpected exceptions after phase 2 starts and before verified shutdown block the lease without automatic TTL or recovery. Supervision confirms CoreSimulator Shutdown state without arbitrary host script process-tree guarantees. Output uses schema version 3, UI snapshots are invalidated upon admission, and next-step templates are suppressed. Normal MCP and native session resume remain available without an external resume script.
outputSchema:
schema: xcodebuildmcp.output.test-result
version: "3"
version: '3'
predicates:
- hideWhenXcodeAgentMode
annotations:
Expand Down
4 changes: 2 additions & 2 deletions manifests/tools/type_text.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ names:
mcp: type_text
cli: type-text
description: >-
Type text into a UI element by elementRef from a current rs/1 runtime snapshot, optionally replacing existing field contents. elementRef is required; do not call with only text. Example input: {"elementRef":"e8","text":"London","replaceExisting":true}.
Type text into a UI element by elementRef from a current rs/1 runtime snapshot, optionally replacing existing field contents. In managed mode, requires operation credentials and a ref from this operation and runtime; use fresh refs returned after each action. elementRef is required; do not call with only text. Example input: {"elementRef":"e8","text":"London","replaceExisting":true}.
outputSchema:
schema: xcodebuildmcp.output.ui-action-result
version: "2"
version: '2'
routing:
stateful: true
annotations:
Expand Down
12 changes: 12 additions & 0 deletions manifests/workflows/resource-management.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
id: resource-management
title: Simulator Resource Management
description: Explicit operation leases for a persistent worktree Simulator.
targetPlatforms: []
predicates:
- managedResourcesEnabled
selection:
mcp:
autoInclude: true
tools:
- resource_operation
- resource_provision
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://xcodebuildmcp.com/schemas/structured-output/xcodebuildmcp.output.resource-binding/1.schema.json",
"type": "object",
"additionalProperties": false,
"allOf": [
{
"$ref": "https://xcodebuildmcp.com/schemas/structured-output/_defs/common.schema.json#/$defs/errorConsistency"
},
{
"if": {
"properties": {
"didError": {
"const": false
}
}
},
"then": {
"properties": {
"data": {
"properties": {
"simulatorId": {
"type": "string"
}
},
"type": "object"
}
}
}
}
],
"properties": {
"schema": {
"const": "xcodebuildmcp.output.resource-binding"
},
"schemaVersion": {
"const": "1"
},
"didError": {
"type": "boolean"
},
"error": {
"type": [
"string",
"null"
]
},
"data": {
"type": "object",
"additionalProperties": false,
"properties": {
"worktreeGeneration": {
"type": "string",
"pattern": "^[a-f0-9]{64}$"
},
"simulatorId": {
"type": [
"string",
"null"
]
},
"deviceType": {
"type": "string"
},
"runtime": {
"type": "string"
}
},
"required": [
"worktreeGeneration",
"simulatorId",
"deviceType",
"runtime"
]
},
"nextSteps": {
"$ref": "https://xcodebuildmcp.com/schemas/structured-output/_defs/common.schema.json#/$defs/nextSteps"
}
},
"required": [
"schema",
"schemaVersion",
"didError",
"error",
"data"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://xcodebuildmcp.com/schemas/structured-output/xcodebuildmcp.output.resource-operation/1.schema.json",
"type": "object",
"additionalProperties": false,
"allOf": [
{
"$ref": "https://xcodebuildmcp.com/schemas/structured-output/_defs/common.schema.json#/$defs/errorConsistency"
}
],
"properties": {
"schema": {
"const": "xcodebuildmcp.output.resource-operation"
},
"schemaVersion": {
"const": "1"
},
"didError": {
"type": "boolean"
},
"error": {
"type": [
"string",
"null"
]
},
"data": {
"type": "object",
"additionalProperties": false,
"properties": {
"requestId": {
"type": "string"
},
"sessionId": {
"type": "string"
},
"simulatorId": {
"type": "string"
},
"token": {
"type": "string"
},
"reason": {
"type": "string"
},
"action": {
"enum": [
"begin",
"poll",
"status",
"end",
"cancel"
]
},
"state": {
"enum": [
"waiting",
"active",
"closing",
"blocked",
"released",
"cancelled"
]
},
"activityCount": {
"type": "integer",
"minimum": 0
}
},
"required": [
"action",
"requestId",
"sessionId",
"simulatorId",
"state",
"activityCount"
]
},
"nextSteps": {
"$ref": "https://xcodebuildmcp.com/schemas/structured-output/_defs/common.schema.json#/$defs/nextSteps"
}
},
"required": [
"schema",
"schemaVersion",
"didError",
"error",
"data"
]
}
11 changes: 10 additions & 1 deletion src/cli/daemon-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ import {
type XcodeIdeInvokeParams,
type XcodeIdeInvokeResult,
} from '../daemon/protocol.ts';
import { getSocketPath } from '../daemon/socket-path.ts';
import { getSocketPath, assertManagedSocketPath } from '../daemon/socket-path.ts';
import {
resourceEnvironment,
assertResourceNamespace,
} from '../resource-management/environment.ts';
import type { AnyFragment } from '../types/domain-fragments.ts';

export class DaemonVersionMismatchError extends Error {
Expand All @@ -43,6 +47,7 @@ export class DaemonClient {

constructor(opts: DaemonClientOptions = {}) {
this.socketPath = opts.socketPath ?? getSocketPath();
assertManagedSocketPath(this.socketPath);
this.timeout = opts.timeout ?? 30000;
}

Expand Down Expand Up @@ -131,6 +136,7 @@ export class DaemonClient {
* Stop the daemon.
*/
async stop(): Promise<void> {
if (resourceEnvironment()) assertResourceNamespace((await this.status()).resourceNamespace);
await this.request<{ ok: boolean }>('daemon.stop');
}

Expand All @@ -149,6 +155,8 @@ export class DaemonClient {
args: Record<string, unknown>,
options: InvokeToolOptions = {},
): Promise<ToolInvokeResult> {
const managed = resourceEnvironment();
if (managed) assertResourceNamespace((await this.status()).resourceNamespace);
const id = randomUUID();
const req: DaemonRequest<ToolInvokeParams> = {
v: DAEMON_PROTOCOL_VERSION,
Expand All @@ -157,6 +165,7 @@ export class DaemonClient {
params: {
tool,
args,
...(managed ? { resourceNamespace: managed.namespace } : {}),
},
};

Expand Down
Loading