Skip to content

Commit e1e0859

Browse files
cameroncookecodex
andauthored
feat(xcode-ide): Persist bridge response artifacts (#396)
* ref(rendering): Finalize output from structured results Make final tool rendering depend on explicit structured results instead of streaming fragments. This keeps live progress transient and gives CLI, daemon, and MCP output paths the same source of truth for final success, errors, and text rendering. Add a shared structured error shape and schema coverage so failures can be rendered consistently without scraping fragment text. * ref(build-run): Return explicit structured results Update simulator, device, and macOS build-and-run flows to populate final structured results directly. This removes the need to infer final status or artifact paths from progress fragments while preserving live progress for interactive renderers. Adjust resource and test helpers around the new result contract. * fix(filesystem): Prune workspace artifacts safely Expand workspace filesystem cleanup so transient XcodeBuildMCP-owned files are removed with ownership checks and bounded retention. This keeps generated artifacts from accumulating while preserving files that belong to active processes or other workspaces. Add regression coverage for cleanup ownership and retention behavior. * feat(xcode-ide): Persist bridge responses as artifacts Save raw Xcode IDE bridge call results as transient workspace artifacts and return concise structured summaries from the public tools. This prevents large relayed payloads from being embedded in final text output while preserving the full remote response for callers that need it. Route Xcode IDE CLI commands through the generic output pipeline so text, JSON, and JSONL modes share the same behavior as other tool commands. * test(xcode-ide): Add bridge output snapshots Add snapshot coverage for Xcode IDE list-tools and documentation-search output across CLI text, MCP text, and CLI JSON modes. Normalize transient artifact paths so the fixtures verify the public contract without depending on local workspace directories. * docs: Document output and cleanup rules Update agent guidance for structured output terminology, streaming boundaries, and workspace filesystem cleanup. Add the corresponding changelog entries for the Xcode IDE artifact output and fragment-state removal. * test(swift-package): Allow nonzero run duration Avoid asserting an exact wall-clock duration for swift_package_run progress. CI can legitimately report a small nonzero duration even when local runs finish within the same millisecond. * fix(rendering): Prevent fragment state from affecting final output Remove fragment caching from render sessions and test results so streamed fragments cannot be reused as final response state. Normalize xcode-ide tool counts in snapshots to avoid environment-specific fixture churn. Refs GH-360 Co-Authored-By: Codex <noreply@openai.com> --------- Co-authored-by: Codex <noreply@openai.com>
1 parent 839c95f commit e1e0859

72 files changed

Lines changed: 2667 additions & 986 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ ESM TypeScript project (`type: module`). Key layers:
5454
- No `.js` imports in `src/` (enforced by ESLint)
5555
- No barrel imports from `utils/index` - import from specific submodules (e.g., `src/utils/execution/index.ts`, `src/utils/logging/index.ts`)
5656

57+
58+
## Rendering and Streaming Contract
59+
- Streaming fragments are transient output only. They MUST NOT be used as internal state, cached for final responses, or promoted into final MCP/JSON/CLI text output.
60+
- Non-streaming runtimes/output modes, including MCP final responses, MUST render only from the final structured result and next-step metadata. If final output needs data, add it to the final result type instead of reading it from fragments.
61+
- Only streaming-capable renderers may observe fragment callbacks, and only to print live progress. Their fragment handling must not affect final structured output or final rendered text.
62+
5763
## Test Conventions
5864
- Vitest with colocated `__tests__/` directories using `*.test.ts`
5965
- Smoke tests in `src/smoke-tests/__tests__/` (separate Vitest config, serial execution)
@@ -88,6 +94,7 @@ When reading issues:
8894
- Use shared lock and atomic-write helpers for mutable shared files.
8995
- Prefer one-record-per-file registries over shared aggregate files.
9096
- Cleanup must verify ownership before deleting shared artifacts.
97+
- User-facing artifact/log paths in final text or structured output must use `displayPath()` from `src/utils/build-preflight.ts`, so paths are cwd-relative when possible or `~/...` instead of absolute home paths. Keep stored files at their real absolute paths; only normalize response/display values.
9198

9299
## Style
93100
- Keep answers short and concise
@@ -98,6 +105,7 @@ When reading issues:
98105
## Docs
99106
- Do not commit transient investigation notes, prompt exports, or scratch analysis docs after the work is complete.
100107
- If an investigation leaves unresolved follow-up work, move it to a GitHub issue instead of preserving the transient doc in the branch.
108+
- Structured output JSON schemas are auto-published to the website/public schema mirror when merged; do not manually update public schema copies unless explicitly asked.
101109

102110
### Changelog
103111
Location: `CHANGELOG.md`

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414

1515
### Changed
1616

17+
- Updated Xcode IDE `call-tool` output to save raw remote responses as transient workspace-state JSON artifacts, summarize text output without embedding large relayed payloads, and support `--output json` / `--output jsonl` through the generic CLI output path.
1718
- Centralized workspace log retention and startup/shutdown filesystem cleanup so XcodeBuildMCP-owned logs are pruned consistently while preserving active daemon and simulator OSLog outputs.
19+
- Removed internal streaming-fragment context flags so final tool state now comes from explicit structured outputs instead of transient progress fragments ([#360](https://github.com/getsentry/XcodeBuildMCP/issues/360)).
1820

1921
## [2.5.0-beta.1]
2022

CLAUDE.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ When reading issues:
2929
- Use shared lock and atomic-write helpers for mutable shared files.
3030
- Prefer one-record-per-file registries over shared aggregate files.
3131
- Cleanup must verify ownership before deleting shared artifacts.
32+
- User-facing artifact/log paths in final text or structured output must use `displayPath()` from `src/utils/build-preflight.ts`, so paths are cwd-relative when possible or `~/...` instead of absolute home paths. Keep stored files at their real absolute paths; only normalize response/display values.
3233

3334
## Style
3435
- Keep answers short and concise
@@ -39,6 +40,7 @@ When reading issues:
3940
## Docs
4041
- Do not commit transient investigation notes, prompt exports, or scratch analysis docs after the work is complete.
4142
- If an investigation leaves unresolved follow-up work, move it to a GitHub issue instead of preserving the transient doc in the branch.
43+
- Structured output JSON schemas are auto-published to the website/public schema mirror when merged; do not manually update public schema copies unless explicitly asked.
4244

4345
### Changelog
4446
Location: `CHANGELOG.md`
@@ -62,6 +64,12 @@ Use these sections under `## [Unreleased]`:
6264
- **Internal changes (from issues)**: `Fixed foo bar ([#123](https://github.com/cameroncook/XcodeBuildMCP/issues/123))`
6365
- **External contributions**: `Added feature X ([#456](https://github.com/cameroncook/XcodeBuildMCP/pull/456) by [@username](https://github.com/username))`
6466

67+
68+
## Rendering and Streaming Contract
69+
- Streaming fragments are transient output only. They MUST NOT be used as internal state, cached for final responses, or promoted into final MCP/JSON/CLI text output.
70+
- Non-streaming runtimes/output modes, including MCP final responses, MUST render only from the final structured result and next-step metadata. If final output needs data, add it to the final result type instead of reading it from fragments.
71+
- Only streaming-capable renderers may observe fragment callbacks, and only to print live progress. Their fragment handling must not affect final structured output or final rendered text.
72+
6573
## Test Execution Rules
6674
- When running long test suites (snapshot tests, smoke tests), ALWAYS write full output to a log file and read it afterwards. NEVER pipe through `tail` or `grep` directly — that loses output you may need to debug failures.
6775
- Pattern: `DEVICE_ID=... npm run test:snapshot 2>&1 | tee /tmp/snapshot-results.txt` then read `/tmp/snapshot-results.txt` with the native read tool.

manifests/tools/xcode_ide_call_tool.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ names:
66
description: Call a remote Xcode IDE MCP tool.
77
outputSchema:
88
schema: xcodebuildmcp.output.xcode-bridge-call-result
9-
version: "1"
9+
version: '2'
1010
routing:
1111
stateful: true
1212
annotations:

manifests/tools/xcode_ide_list_tools.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ names:
66
description: "Lists Xcode-IDE-only MCP capabilities (Use for: SwiftUI previews image capture, code snippet execution, issue Navigator/build logs, and window/tab context)."
77
outputSchema:
88
schema: xcodebuildmcp.output.xcode-bridge-tool-list
9-
version: "1"
9+
version: "2"
1010
routing:
1111
stateful: true
1212
annotations:
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$id": "https://xcodebuildmcp.com/schemas/structured-output/xcodebuildmcp.output.error/1.schema.json",
4+
"type": "object",
5+
"additionalProperties": false,
6+
"allOf": [
7+
{
8+
"$ref": "https://xcodebuildmcp.com/schemas/structured-output/_defs/common.schema.json#/$defs/errorConsistency"
9+
}
10+
],
11+
"properties": {
12+
"schema": {
13+
"const": "xcodebuildmcp.output.error"
14+
},
15+
"schemaVersion": {
16+
"const": "1"
17+
},
18+
"didError": {
19+
"const": true
20+
},
21+
"error": {
22+
"type": "string",
23+
"minLength": 1
24+
},
25+
"data": {
26+
"type": "object",
27+
"additionalProperties": false,
28+
"properties": {
29+
"category": {
30+
"enum": ["runtime", "validation", "schema"]
31+
},
32+
"code": {
33+
"type": "string",
34+
"minLength": 1
35+
}
36+
},
37+
"required": ["category", "code"]
38+
}
39+
},
40+
"required": ["schema", "schemaVersion", "didError", "error", "data"]
41+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$id": "https://xcodebuildmcp.com/schemas/structured-output/xcodebuildmcp.output.xcode-bridge-call-result/2.schema.json",
4+
"type": "object",
5+
"additionalProperties": false,
6+
"allOf": [
7+
{
8+
"$ref": "https://xcodebuildmcp.com/schemas/structured-output/_defs/common.schema.json#/$defs/errorConsistency"
9+
}
10+
],
11+
"$defs": {
12+
"relayedContentItem": {
13+
"type": "object",
14+
"additionalProperties": true,
15+
"properties": {
16+
"type": { "type": "string" }
17+
},
18+
"required": ["type"]
19+
},
20+
"artifacts": {
21+
"type": "object",
22+
"additionalProperties": false,
23+
"properties": {
24+
"rawResponseJsonPath": { "type": "string" }
25+
},
26+
"required": ["rawResponseJsonPath"]
27+
}
28+
},
29+
"properties": {
30+
"schema": { "const": "xcodebuildmcp.output.xcode-bridge-call-result" },
31+
"schemaVersion": { "const": "2" },
32+
"didError": { "type": "boolean" },
33+
"error": { "type": ["string", "null"] },
34+
"data": {
35+
"type": "object",
36+
"additionalProperties": false,
37+
"properties": {
38+
"remoteTool": { "type": "string" },
39+
"succeeded": { "type": "boolean" },
40+
"content": {
41+
"type": "array",
42+
"items": { "$ref": "#/$defs/relayedContentItem" }
43+
},
44+
"artifacts": { "$ref": "#/$defs/artifacts" }
45+
},
46+
"required": ["remoteTool", "succeeded", "content"]
47+
}
48+
},
49+
"required": ["schema", "schemaVersion", "didError", "error", "data"]
50+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$id": "https://xcodebuildmcp.com/schemas/structured-output/xcodebuildmcp.output.xcode-bridge-tool-list/2.schema.json",
4+
"type": "object",
5+
"additionalProperties": false,
6+
"allOf": [
7+
{
8+
"$ref": "https://xcodebuildmcp.com/schemas/structured-output/_defs/common.schema.json#/$defs/errorConsistency"
9+
}
10+
],
11+
"$defs": {
12+
"artifacts": {
13+
"type": "object",
14+
"additionalProperties": false,
15+
"properties": {
16+
"rawResponseJsonPath": { "type": "string" }
17+
},
18+
"required": ["rawResponseJsonPath"]
19+
}
20+
},
21+
"properties": {
22+
"schema": { "const": "xcodebuildmcp.output.xcode-bridge-tool-list" },
23+
"schemaVersion": { "const": "2" },
24+
"didError": { "type": "boolean" },
25+
"error": { "type": ["string", "null"] },
26+
"data": {
27+
"type": "object",
28+
"additionalProperties": false,
29+
"properties": {
30+
"toolCount": { "type": "integer", "minimum": 0 },
31+
"artifacts": { "$ref": "#/$defs/artifacts" }
32+
},
33+
"required": ["toolCount"]
34+
}
35+
},
36+
"required": ["schema", "schemaVersion", "didError", "error", "data"]
37+
}

src/cli.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,23 @@ function findTopLevelCommand(argv: string[]): string | undefined {
3232
return undefined;
3333
}
3434

35+
function findGlobalSocketOverride(argv: string[]): string | undefined {
36+
for (let index = 0; index < argv.length; index += 1) {
37+
const token = argv[index];
38+
if (token === '--socket') {
39+
const value = argv[index + 1];
40+
return value && !value.startsWith('-') ? value : undefined;
41+
}
42+
43+
if (token.startsWith('--socket=')) {
44+
const value = token.slice('--socket='.length);
45+
return value || undefined;
46+
}
47+
}
48+
49+
return undefined;
50+
}
51+
3552
async function buildLightweightYargsApp(): Promise<ReturnType<typeof import('yargs').default>> {
3653
const yargs = (await import('yargs')).default;
3754
const { hideBin } = await import('yargs/helpers');
@@ -120,10 +137,12 @@ async function main(): Promise<void> {
120137

121138
const { workspaceRoot, workspaceKey } = result;
122139

123-
const defaultSocketPath = getSocketPath({
124-
cwd: result.runtime.cwd,
125-
projectConfigPath: result.configPath,
126-
});
140+
const socketPath =
141+
findGlobalSocketOverride(process.argv.slice(2)) ??
142+
getSocketPath({
143+
cwd: result.runtime.cwd,
144+
projectConfigPath: result.configPath,
145+
});
127146

128147
const cliExposedWorkflowIds = await listCliWorkflowIdsFromManifest({
129148
excludeWorkflows: ['session-management', 'workflow-discovery'],
@@ -133,7 +152,7 @@ async function main(): Promise<void> {
133152

134153
// CLI uses a manifest-resolved catalog plus daemon-backed xcode-ide dynamic tools.
135154
const catalog = await buildCliToolCatalog({
136-
socketPath: defaultSocketPath,
155+
socketPath,
137156
workspaceRoot,
138157
cliExposedWorkflowIds,
139158
discoveryMode,
@@ -142,7 +161,7 @@ async function main(): Promise<void> {
142161
const yargsApp = buildYargsApp({
143162
catalog,
144163
runtimeConfig: result.runtime.config,
145-
defaultSocketPath,
164+
defaultSocketPath: socketPath,
146165
workspaceRoot,
147166
workspaceKey,
148167
workflowNames: cliExposedWorkflowIds,

0 commit comments

Comments
 (0)