Skip to content

Emit structuredContent on 7 MCP tool handlers#117

Merged
obj-p merged 1 commit intocli-mcp-parityfrom
structured-daemon-responses
Apr 16, 2026
Merged

Emit structuredContent on 7 MCP tool handlers#117
obj-p merged 1 commit intocli-mcp-parityfrom
structured-daemon-responses

Conversation

@obj-p
Copy link
Copy Markdown
Owner

@obj-p obj-p commented Apr 16, 2026

Summary

Wave 2 PR 1/2 per plan in /Users/obj-p/.claude/plans/tingly-twirling-crane.md. Additive: the daemon now emits CallTool.Result.structuredContent alongside existing text content blocks. The CLI still regex-parses text in this PR; PR 2 wires the CLI to decode structured payloads and adds --json.

What changed

New file: Sources/PreviewsCLI/DaemonProtocol.swift — caseless-enum namespace holding Codable DTOs (PreviewStartResult, VariantsResult, SwitchResult, PreviewListResult, SimulatorListResult, SessionListResult, plus shared PreviewInfoDTO and TraitsDTO).

7 handlers migrated in Sources/PreviewsCLI/MCPServer.swift:

  • preview_start / preview_variants / preview_switch / preview_list / simulator_list / session_list — use the Codable init of CallTool.Result to encode the DTO into structuredContent.
  • preview_elements — the WDA accessibility tree is arbitrary nested JSON; parses the existing text payload into Value directly rather than mirroring fields.

Variants addressing: each VariantOutcomeDTO carries imageIndex: Int? pointing into the sibling content: [Tool.Content] array, so the CLI can pair a label with its base64 image without regex-parsing the [N] <label>: preambles.

Text content blocks are preserved verbatim on every handler — MCP clients without structured-content support keep working.

What's deferred

  • CLI migration (delete 3 regex sites, wire decodeStructured, add --json on 7 commands) — PR 2.
  • Imperative tool handlers (preview_stop, preview_configure, preview_touch) — explicitly skipped per plan.

Test plan

  • swift build
  • swift test --filter MacOSMCPTests — 4 tests pass (~56s), including the new structuredContentPayloadsDecode that exercises every migrated tool and asserts each payload decodes through its canonical DTO.
  • Image-index cross-references validated: the test walks each VariantOutcomeDTO.imageIndex and confirms it points to an .image block in the sibling content array.

Package change

  • MCPIntegrationTests now depends on PreviewsCLI so @testable import can reach DaemonProtocol. This mirrors the existing PreviewsCLITests target.

🤖 Generated with Claude Code

The MCP Swift SDK exposes CallTool.Result.structuredContent: Value?
alongside the traditional [Tool.Content] blocks. Populate it on every
tool handler that returns non-trivial data so clients (CLI scripts
via --json, MCP agents via structured consumption) can consume a
typed payload instead of regex-parsing prose.

Shared DTOs live in Sources/PreviewsCLI/DaemonProtocol.swift — a
caseless-enum namespace with nested Codable structs. Kept separate
from domain types (e.g. PreviewInfoDTO vs PreviewsCore.PreviewInfo)
so the wire contract can drift independently.

Migrated handlers:
- preview_start → PreviewStartResult { sessionID, platform,
  sourceFilePath, deviceUDID?, pid?, traits?, previews[], activeIndex,
  setupWarning? }
- preview_variants → VariantsResult { variants[], successCount,
  failCount }. Each variant carries status="ok"|"error" plus
  imageIndex pointing into the sibling content array (for success) or
  an error string (for failure).
- preview_switch → SwitchResult { sessionID, activeIndex, traits?,
  previews[] }. previews[].active replaces the " <- active" marker.
- preview_list → PreviewListResult { file, previews[] }.
- simulator_list → SimulatorListResult { simulators[] }.
- session_list → SessionListResult { sessions[] }. The tab-delimited
  text body is now rendered from the DTO list.
- preview_elements → { sessionID, elements: <tree> } as a raw Value.
  The accessibility tree is opaque WDA JSON, round-tripped natively
  rather than mirrored field-by-field.

Text content blocks are preserved verbatim on every migrated handler
so MCP clients without structured-content support keep working.

Additive change — the CLI still regex-parses text for now. CLI
migration to decodeStructured + --json lands in the follow-up PR.

Tests:
- New MacOSMCPTests.structuredContentPayloadsDecode exercises
  every migrated tool against the real daemon and decodes each
  response through the canonical DTO. Image-index references are
  validated against the sibling content array.
- MCPIntegrationTests now depends on PreviewsCLI so @testable can
  import DaemonProtocol.
- MCPTestServer.callToolResult + decodeStructured helpers added.

All 4 MacOSMCPTests pass (~56s).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@obj-p obj-p merged commit b119e63 into cli-mcp-parity Apr 16, 2026
@obj-p obj-p deleted the structured-daemon-responses branch April 16, 2026 00:13
obj-p added a commit that referenced this pull request Apr 16, 2026
Wave 2 PR 2/2. The daemon started emitting `structuredContent` in
#117; this PR wires the CLI to decode those payloads and adds the
`--json` output mode per the plan.

Shared helpers in `MCPContentHelpers.swift`:
- `Client.callToolStructured(name:arguments:)` — returns the full
  `CallTool.Result` including `structuredContent`. The SDK's
  tuple-returning `callTool` overload drops that field.
- `Value.decode(_:)` — decode an MCP `Value` into a `Codable`.
- `emitJSON(_:)` — write a pretty-printed, sorted-keys JSON
  document to stdout. Used by every `--json` mode.

Regex parsers deleted (3 call sites):
- `RunCommand.extractSessionID`
- `SnapshotCommand.extractSessionID`
- `VariantsCommand.extractSessionID`
- `VariantsCommand.parseSuccessPreamble`
- `VariantsCommand.parseFailurePreamble`
Every site that needed the sessionID now decodes
`PreviewStartResult.sessionID` from `structuredContent`.

`VariantsCommand.captureVariants` rewrite: instead of walking the
text/image content interleave with regex, decode `VariantsResult`
and use each `VariantOutcomeDTO.imageIndex` to index into the
sibling content array. The Critical ERROR-in-label bug from #109 is
structurally unreachable now — outcome classification comes from a
typed `status` field, not a substring match on prose.

`--json` added to 7 read-oriented commands (per plan):
- `run --detach --json` → full `PreviewStartResult`.
- `snapshot --json` → `{ sessionID, outputPath, format, bytes }`
  synthesized client-side.
- `variants --json` → `{ variants[], successCount, failCount }` with
  per-variant status/path/error. Files still written to disk.
- `list --json` → `PreviewListResult` (client-side only; list does
  not hit the daemon).
- `status --json` → `{ state, running, pid, socketPath }`
  synthesized client-side.
- `simulators --json` → daemon's `SimulatorListResult` passthrough.
- `elements --json` → `{ sessionID, elements }` envelope instead of
  the bare tree.

Skipped on imperative commands (`stop`, `touch`, `configure`,
`switch`, `kill-daemon`) per plan — nothing worth structuring on
stdout.

All 44 tests across 7 suites green. Smoke-tested end-to-end:
- `run --detach --json` → emits full PreviewStartResult
- `snapshot --json` → emits outputPath+bytes+format
- `variants --json` → emits per-variant status/path
- `status --json`, `list --json`, `simulators --json` work offline

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
obj-p added a commit that referenced this pull request Apr 16, 2026
…118)

Wave 2 PR 2/2. The daemon started emitting `structuredContent` in
#117; this PR wires the CLI to decode those payloads and adds the
`--json` output mode per the plan.

Shared helpers in `MCPContentHelpers.swift`:
- `Client.callToolStructured(name:arguments:)` — returns the full
  `CallTool.Result` including `structuredContent`. The SDK's
  tuple-returning `callTool` overload drops that field.
- `Value.decode(_:)` — decode an MCP `Value` into a `Codable`.
- `emitJSON(_:)` — write a pretty-printed, sorted-keys JSON
  document to stdout. Used by every `--json` mode.

Regex parsers deleted (3 call sites):
- `RunCommand.extractSessionID`
- `SnapshotCommand.extractSessionID`
- `VariantsCommand.extractSessionID`
- `VariantsCommand.parseSuccessPreamble`
- `VariantsCommand.parseFailurePreamble`
Every site that needed the sessionID now decodes
`PreviewStartResult.sessionID` from `structuredContent`.

`VariantsCommand.captureVariants` rewrite: instead of walking the
text/image content interleave with regex, decode `VariantsResult`
and use each `VariantOutcomeDTO.imageIndex` to index into the
sibling content array. The Critical ERROR-in-label bug from #109 is
structurally unreachable now — outcome classification comes from a
typed `status` field, not a substring match on prose.

`--json` added to 7 read-oriented commands (per plan):
- `run --detach --json` → full `PreviewStartResult`.
- `snapshot --json` → `{ sessionID, outputPath, format, bytes }`
  synthesized client-side.
- `variants --json` → `{ variants[], successCount, failCount }` with
  per-variant status/path/error. Files still written to disk.
- `list --json` → `PreviewListResult` (client-side only; list does
  not hit the daemon).
- `status --json` → `{ state, running, pid, socketPath }`
  synthesized client-side.
- `simulators --json` → daemon's `SimulatorListResult` passthrough.
- `elements --json` → `{ sessionID, elements }` envelope instead of
  the bare tree.

Skipped on imperative commands (`stop`, `touch`, `configure`,
`switch`, `kill-daemon`) per plan — nothing worth structuring on
stdout.

All 44 tests across 7 suites green. Smoke-tested end-to-end:
- `run --detach --json` → emits full PreviewStartResult
- `snapshot --json` → emits outputPath+bytes+format
- `variants --json` → emits per-variant status/path
- `status --json`, `list --json`, `simulators --json` work offline

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant