Transit PR #238 head → base 009d9af → c0fa884 scope 7 files · +415 / −12 CI claude-review PASS threads 0 unresolved

Review: T-1821 MCP port listener alignment

Independent exact-head verification of PR #238 · 009d9af into c0fa884

At a glance

  • The exact requested head is 009d9af22b1c23214635eb7eb5af1fc49f8f1642 and its PR base is c0fa884ccfd1de4d21e98f83b5f88abd56f1fb9b.
  • The prior P1 changelog collision is resolved: both T-1824 and T-1821 are retained under Unreleased.
  • GitHub’s fresh claude-review job completed successfully for this exact SHA, and GraphQL reports a clean, mergeable PR with zero unresolved threads.

Verdict

Ready / LGTM

PASS. The required P1 is fixed: the exact merge diff preserves the already-merged T-1824 Unreleased entry and adds the final T-1821 entry immediately after it. No actionable findings remain.

Author's PR description

Shown verbatim — the markdown the author wrote, unmodified.

@ArjenSchwarz · 2026-08-05
## Summary
- Route committed MCP Settings port edits, including focus loss and view teardown, through the existing desired-state listener lifecycle.
- Keep setup instructions aligned with `MCPServer.activePort`; cancel pending edits when MCP is disabled.
- Add testable coordinator state and a live loopback focus-loss regression.

## Root cause
The Settings port binding persisted independently, while listener restart was limited to `TextField.onSubmit`, leaving a focus-loss commit advertised but not bound.

## Validation
- `make test-quick` ✅
- `make lint` ✅
- `make test` ⚠️ two attempts timed out; the second reported unrelated UI failures in `testClearAll`, `testEditViewPreservesTaskMilestone`, and `testDataMaintenanceGoldenPath` before timeout. T-1821 is macOS-only.

See `specs/bugfixes/mcp-port-listener-alignment/report.md`.

Commits

Three-level explanation

Changing the MCP port and leaving the field without pressing Return now updates the running server too. The connection command continues to show the port that is actually listening, rather than a saved value that has not yet taken effect.

The Settings view forwards committed port changes to a small debouncing coordinator. That coordinator deliberately owns only pending UI work; it delegates all listener start, stop, replacement, and invalid-port behavior to MCPServer's existing serialized desired-state lifecycle.

MCPPortChangeState remains a pure nonisolated value type for deterministic deduplication and cancellation tests, while the MainActor coordinator serializes debounce/flush behavior with the UI. Clearing pending state synchronously before stop makes a late cancelled debounce consume no port. The loopback regression proves the actual focus-loss path replaces the socket binding.

Important changes — detailed

CHANGELOG: preserve both release notes

CHANGELOG.md

Why it matters. Prevents merging PR #238 from dropping the already-merged T-1824 release note.

What to look at. Unreleased entries

Takeaway. A release-note-only conflict still needs review against the PR’s actual merge base.
Rationale. The final exact-head commit adds T-1821 after the T-1824 entry from the supplied base.

MCP port coordinator: route committed field values through listener lifecycle

Transit/Transit/MCP/MCPPortChangeCoordinator.swift

Why it matters. Fixes the focus-loss and Settings-teardown path that previously persisted a port without moving the listener.

What to look at. MCPPortChangeState and MCPPortChangeCoordinator

Takeaway. Keep UI debounce state separate from the owner of an asynchronous resource lifecycle.
Rationale. Existing MCPServer.start(port:) already serializes replacement, invalid-port handling, and teardown.

Settings and live loopback test: prove active endpoint convergence

Transit/Transit/Views/Settings/SettingsView.swift

Why it matters. Ensures the displayed setup command follows the active listener and the actual focus-loss path is covered.

What to look at. onChange, cancellation/flush, activePort presentation, and focusLossPortCommitReplacesLiveListenerWithoutSubmit

Takeaway. For a UI lifecycle defect, retain a live resource-level regression in addition to pure state tests.
Rationale. The test starts the listener, commits a new port without onSubmit, then verifies the new port binds and the old one is released.

Key decisions

Reuse the serialized MCP listener lifecycle

The coordinator forwards to MCPServer.start(port:) rather than duplicating start/stop/rebind logic. This preserves the established desired-state coordinator as the single listener owner.

Present the confirmed endpoint, not the persisted draft

The setup command reads activePort, so it cannot direct clients to a replacement that has not become live.

Per-file diffs

Click to expand.

CHANGELOG.md Modified +1 / -0
(no diff provided)
Transit/Transit/MCP/MCPPortChangeCoordinator.swift Added +104 / -0
(no diff provided)
Transit/Transit/MCP/MCPServer.swift Modified +1 / -1
(no diff provided)
Transit/Transit/Views/Settings/SettingsView.swift Modified +27 / -10
(no diff provided)
Transit/TransitTests/MCPPortChangeCoordinatorTests.swift Added +169 / -0
(no diff provided)
specs/bugfixes/mcp-port-listener-alignment/report.md Added +112 / -0
(no diff provided)
specs/mcp-server/implementation.md Modified +1 / -1
(no diff provided)