Skip to content

Split MCPServer.swift into per-tool handler files behind ToolHandler#162

Merged
obj-p merged 2 commits intomainfrom
worktree-handler-split
May 6, 2026
Merged

Split MCPServer.swift into per-tool handler files behind ToolHandler#162
obj-p merged 2 commits intomainfrom
worktree-handler-split

Conversation

@obj-p
Copy link
Copy Markdown
Owner

@obj-p obj-p commented May 6, 2026

Summary

Step #1 of the architectural-refactor roadmap. The previous shape kept three sources of truth in lockstep: the `ToolName` enum, the `mcpToolSchemas()` array, and the `switch tool` dispatch in `configureMCPServer`. Adding a tool meant touching all three.

Introduces a `ToolHandler` protocol with `static var name`, `static var schema`, and `static func handle(_:ctx:)`. Each of the 12 tools moves to its own file under `Sources/PreviewsCLI/Handlers/`, carrying the schema next to the implementation. `configureMCPServer` derives both the `ListTools` response and the `CallTool` dispatch from a single `handlerRegistry` array — the array IS the inventory. Adding a tool now means writing one file plus appending one line.

`HandlerContext` bundles the engine-layer dependencies (host, iosState, configCache, router, macCompiler, server) so handlers receive them via parameter instead of reaching for file-private vars. Static handler types are trivially `Sendable`.

Sizes

  • `MCPServer.swift`: 1131 → 92 lines (target was <200 ✓)
  • `MCPToolSchemas.swift`: deleted (366 lines moved into per-handler `static var schema`)
  • New: 13 files in `Handlers/` (1 protocol + 1 context + 1 ToolName + 12 handlers)
  • New: `MCPServerSupport.swift` for shared infra (`MCPProgressReporter`, `runMCPServer`, `advertisedServerVersion`, `parseTraits`, `configQualityForSession`)

Acceptance gate

The plan's acceptance criterion — "`ListTools` response bytes are identical pre/post" — is enforced by a new `ListToolsSnapshotTests` snapshot. The pre-refactor JSON was captured on the prior commit (a068f20) and stored as `Tests/PreviewsCLITests/list_tools_snapshot.json`. The post-refactor registry produces byte-identical output. Future contributors who change a schema must intentionally re-bless the snapshot, making the change visible in the diff.

Behavior preserved

No functional change. Same wire schemas, same handlers, same dispatch — just split across files and threaded through a context bundle.

Test plan

  • `swift build` clean, no warnings
  • `swift-format lint --strict --recursive Sources/PreviewsCLI/ Tests/PreviewsCLITests/` clean
  • `swift test --filter ListToolsSnapshotTests` — wire bytes match
  • `swift test --filter MacOSMCPTests` — 7 tests pass (full workflow, structuredContent decode, variants, hot-reload, heartbeat, withTimeout)
  • `swift test --filter IOSMCPTests` — 2 tests pass (full iOS workflow incl. start, snapshot, elements, tap, swipe, switch)
  • `swift test --filter SwitchCommandTests --filter ConfigureCommandTests --filter StopCommandTests --filter ListCommandTests --filter SnapshotCommandTests --filter VariantsCommandTests` — all CLI integration tests pass
  • `swift test --filter PreviewsCLITests` — full unit suite (incl. `SelfPath`, `StallTimer`, snapshot test) passes

🤖 Generated with Claude Code

obj-p and others added 2 commits May 5, 2026 23:27
Step #1 of the architectural roadmap. The previous shape had three
sources of truth that had to stay in lockstep: the `ToolName` enum,
the `mcpToolSchemas()` array in MCPToolSchemas.swift, and the
`switch tool` dispatch in `configureMCPServer`. Adding a tool meant
touching three places.

Introduce a `ToolHandler` protocol with `static var name`, `static var
schema`, and `static func handle(_:ctx:)`. Each of the 12 tools moves
to its own file under `Sources/PreviewsCLI/Handlers/`, carrying the
schema next to the implementation. `configureMCPServer` derives both
the `ListTools` response and the `CallTool` dispatch from a single
`handlerRegistry` array — the array IS the inventory.

`HandlerContext` bundles the engine-layer dependencies (host, iosState,
configCache, router, macCompiler, server) so handlers receive deps via
parameter rather than reaching for file-private vars. Static handler
types are `Sendable`-trivially since they have no instance state.

`MCPServer.swift` shrinks from 1131 to 92 lines (target was <200);
shared infrastructure moves to `MCPServerSupport.swift` (heartbeat,
progress reporter, version advertisement, `parseTraits`,
`configQualityForSession`); `MCPToolSchemas.swift` is deleted.

The acceptance gate from the architectural plan — "ListTools response
bytes are identical pre/post" — is enforced by a new
`ListToolsSnapshotTests` snapshot that pins the JSON-encoded schemas
against a fixture file. The pre-refactor bytes were captured on the
prior commit, and the test passes against the post-refactor registry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two small comment additions from the code-reviewer pass:

- Document why `Dictionary(uniqueKeysWithValues:)` is the right
  fail-fast for a future contributor who copies a handler and forgets
  to update `static let name` — the trap fires at server construction
  (visible in tests) rather than on the first call.
- Explain the bless-mode early-return in `ListToolsSnapshotTests` —
  we deliberately skip the equality check after writing the snapshot
  because it would tautologically pass; the issue-record is the
  signal to commit the new fixture.

No behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@obj-p obj-p merged commit 3922695 into main May 6, 2026
4 checks passed
@obj-p obj-p deleted the worktree-handler-split branch May 6, 2026 04:08
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