diff --git a/README.md b/README.md index fb97854b..5cbc68dd 100644 --- a/README.md +++ b/README.md @@ -467,6 +467,17 @@ Open http://localhost:8080 in your browser to view the dashboard. See [Web UI Setup Guide](docs/webui-setup.md) for detailed configuration. +## Known Issues + +- **BUG-T5 → FU-P13-T7 (P0):** Empty-content tool results can still violate strict `structuredContent` expectations in strict MCP clients. +- **BUG-T6 → FU-P13-T8 (P0):** Web UI port collisions can happen when multiple MCP sessions start with the same `--web-ui-port` (for example `8080`), producing `address already in use`. +- **BUG-T7 → FU-P13-T9 (P0):** `resources/list` and `resources/templates/list` probing may return non-standard error shapes in some client paths. +- **BUG-T3 (resolved):** If dashboard access is needed independently from MCP startup, run `--web-ui-only` for standalone diagnostics. + +### Disclaimer (Codex App) + +`mcpbridge-wrapper` normalizes Xcode MCP responses, but it does not control Codex App internals. Codex App transport/session behavior may change independently from Codex CLI and from this wrapper. If App and CLI differ, treat that as client-specific behavior first and verify with exact versions, config, and logs. + ## Documentation - [Installation Guide](docs/installation.md) diff --git a/SPECS/ARCHIVE/BUG-T3_webui_only_dashboard_mode/BUG-T3_Validation_Report.md b/SPECS/ARCHIVE/BUG-T3_webui_only_dashboard_mode/BUG-T3_Validation_Report.md new file mode 100644 index 00000000..ba95cb6c --- /dev/null +++ b/SPECS/ARCHIVE/BUG-T3_webui_only_dashboard_mode/BUG-T3_Validation_Report.md @@ -0,0 +1,41 @@ +# BUG-T3 Validation Report + +- **Task ID:** BUG-T3 +- **Task Name:** Web UI cannot stay available when MCP bridge initialization fails +- **Date:** 2026-02-14 +- **Verdict:** PASS + +## Scope Validated + +- Added standalone `--web-ui-only` startup mode in `__main__.py`. +- Verified standalone mode skips bridge startup and runs dashboard server. +- Verified `--web-ui-only` honors custom port and implies Web UI enabled. +- Added troubleshooting guidance for standalone dashboard diagnostics. + +## Acceptance Criteria Check + +1. `--web-ui-only` is accepted by argument parser. + **Result:** PASS +2. `--web-ui-only` implies Web UI enabled and honors `--web-ui-port`. + **Result:** PASS +3. Standalone mode does not start `create_bridge()` or stdin forwarding threads. + **Result:** PASS +4. Existing non-standalone behavior remains unchanged. + **Result:** PASS +5. Required quality gates pass (`pytest`, `ruff check src/`, `mypy src/`, `pytest --cov >= 90%`). + **Result:** PASS + +## Quality Gate Results + +- `pytest` + - Result: PASS (`348 passed, 5 skipped`) +- `ruff check src/` + - Result: PASS (`All checks passed!`) +- `mypy src/` + - Result: PASS (`Success: no issues found in 12 source files`) +- `pytest --cov` + - Result: PASS (`Total coverage: 96.31%`, required `>= 90%`) + +## Notes + +- Test runs emitted non-blocking dependency deprecation warnings from `websockets`/`uvicorn`; no functional regressions were detected. diff --git a/SPECS/ARCHIVE/BUG-T3_webui_only_dashboard_mode/BUG-T3_webui_only_dashboard_mode.md b/SPECS/ARCHIVE/BUG-T3_webui_only_dashboard_mode/BUG-T3_webui_only_dashboard_mode.md new file mode 100644 index 00000000..737f148b --- /dev/null +++ b/SPECS/ARCHIVE/BUG-T3_webui_only_dashboard_mode/BUG-T3_webui_only_dashboard_mode.md @@ -0,0 +1,69 @@ +# BUG-T3 PRD — webui only dashboard mode + +## 1. Context + +Users enabling `--web-ui` for MCP debugging can lose dashboard access when MCP bridge startup fails or when the MCP client session exits early. In those cases the process terminates and Safari shows "can't connect to server" for the dashboard URL. + +## 2. Goal + +Provide an explicit standalone mode that runs only the Web UI server so the dashboard remains reachable during MCP connection diagnostics. + +## 3. Scope + +In scope: +- Add a CLI argument (`--web-ui-only`) in startup parsing +- Ensure standalone mode starts FastAPI Web UI without launching bridge subprocess +- Keep existing `--web-ui` behavior unchanged for normal MCP wrapper mode +- Add unit tests for parsing and main startup behavior +- Document standalone usage for troubleshooting + +Out of scope: +- Changes to MCP protocol transformation behavior +- Changes to Xcode bridge handshake handling +- New dashboard features unrelated to startup mode + +## 4. Deliverables + +- Updated `src/mcpbridge_wrapper/__main__.py`: + - Parse `--web-ui-only` + - Run `run_server(...)` and return in standalone mode +- Updated tests in `tests/unit/test_main_webui.py` +- Updated troubleshooting docs with standalone mode guidance +- Validation report at `SPECS/INPROGRESS/BUG-T3_Validation_Report.md` + +## 5. Acceptance Criteria + +1. `--web-ui-only` is accepted by argument parser. +2. `--web-ui-only` implies Web UI enabled and honors `--web-ui-port`. +3. In standalone mode, wrapper does not start `create_bridge()` or stdin forwarding threads. +4. Existing non-standalone behavior remains unchanged. +5. Quality gates pass: + - `pytest` + - `ruff check src/` + - `mypy src/` + - `pytest --cov` with coverage >= 90% + +## 6. Dependencies + +- Existing Web UI server entrypoint (`mcpbridge_wrapper.webui.server:run_server`) +- Existing CLI parser logic in `src/mcpbridge_wrapper/__main__.py` + +## 7. Implementation Plan + +1. Extend web UI arg parser return shape to include standalone mode flag. +2. Update main startup flow to short-circuit into `run_server(...)` for standalone mode. +3. Add parser and main-path unit tests for standalone mode. +4. Update troubleshooting docs with standalone command examples. +5. Execute required quality gates and capture results. + +## 8. Risks and Mitigations + +- Risk: starting both thread mode and standalone mode could double-bind ports. +- Mitigation: ensure standalone path returns before any bridge/websocket thread startup. + +- Risk: behavior regression in existing `--web-ui` MCP mode. +- Mitigation: keep existing code path intact and covered by current tests. + +--- +**Archived:** 2026-02-14 +**Verdict:** PASS diff --git a/SPECS/ARCHIVE/BUG-T3_webui_only_dashboard_mode/REVIEW_BUG-T3_webui_only_mode.md b/SPECS/ARCHIVE/BUG-T3_webui_only_dashboard_mode/REVIEW_BUG-T3_webui_only_mode.md new file mode 100644 index 00000000..a9c9aa41 --- /dev/null +++ b/SPECS/ARCHIVE/BUG-T3_webui_only_dashboard_mode/REVIEW_BUG-T3_webui_only_mode.md @@ -0,0 +1,29 @@ +## REVIEW REPORT — BUG-T3 webui-only mode + +**Scope:** origin/main..HEAD +**Files:** 8 + +### Summary Verdict +- [x] Approve +- [ ] Approve with comments +- [ ] Request changes +- [ ] Block + +### Critical Issues +- None. + +### Secondary Issues +- None. + +### Architectural Notes +- Standalone dashboard mode is correctly isolated from bridge startup, which removes the lifecycle coupling that caused dashboard availability loss during MCP handshake failures. + +### Tests +- Quality gates executed and passed: + - `pytest` passed (`348 passed, 5 skipped`) + - `ruff check src/` passed + - `mypy src/` passed + - `pytest --cov` passed with `96.31%` coverage (>= 90%) + +### Next Steps +- FOLLOW-UP skipped: no actionable findings requiring new backlog tasks. diff --git a/SPECS/ARCHIVE/INDEX.md b/SPECS/ARCHIVE/INDEX.md index 08483563..8d2cfc08 100644 --- a/SPECS/ARCHIVE/INDEX.md +++ b/SPECS/ARCHIVE/INDEX.md @@ -84,6 +84,7 @@ | FU-P9-T4-1 | [FU-P9-T4-1_Align_publish_helper_output_with_protected_main_branch_workflow/](FU-P9-T4-1_Align_publish_helper_output_with_protected_main_branch_workflow/) | 2026-02-13 | PASS | | FU-P9-T2-2 | [FU-P9-T2-2_Add_troubleshooting_guidance_for_stale_uvx_cache_process_versions/](FU-P9-T2-2_Add_troubleshooting_guidance_for_stale_uvx_cache_process_versions/) | 2026-02-13 | PASS | | BUG-T2 | [BUG-T2_codex_mcp_add_with_Web_UI_extras_fails_in_zsh/](BUG-T2_codex_mcp_add_with_Web_UI_extras_fails_in_zsh/) | 2026-02-14 | PASS | +| BUG-T3 | [BUG-T3_webui_only_dashboard_mode/](BUG-T3_webui_only_dashboard_mode/) | 2026-02-14 | PASS | ## Historical Artifacts @@ -133,6 +134,7 @@ | [REVIEW_fu_p9_t4_1_publish_helper_protected_main.md](FU-P9-T4-1_Align_publish_helper_output_with_protected_main_branch_workflow/REVIEW_fu_p9_t4_1_publish_helper_protected_main.md) | Review report for FU-P9-T4-1 | | [REVIEW_fu_p9_t2_2_stale_uvx_troubleshooting.md](FU-P9-T2-2_Add_troubleshooting_guidance_for_stale_uvx_cache_process_versions/REVIEW_fu_p9_t2_2_stale_uvx_troubleshooting.md) | Review report for FU-P9-T2-2 | | [REVIEW_BUG-T2_zsh_webui_extras.md](BUG-T2_codex_mcp_add_with_Web_UI_extras_fails_in_zsh/REVIEW_BUG-T2_zsh_webui_extras.md) | Review report for BUG-T2 | +| [REVIEW_BUG-T3_webui_only_mode.md](BUG-T3_webui_only_dashboard_mode/REVIEW_BUG-T3_webui_only_mode.md) | Review report for BUG-T3 | ## Archive Log @@ -220,3 +222,5 @@ | 2026-02-13 | FU-P9-T4-1 | Archived REVIEW_fu_p9_t4_1_publish_helper_protected_main report | | 2026-02-14 | BUG-T2 | Archived codex_mcp_add_with_Web_UI_extras_fails_in_zsh (PASS) | | 2026-02-14 | BUG-T2 | Archived REVIEW_BUG-T2_zsh_webui_extras report | +| 2026-02-14 | BUG-T3 | Archived webui_only_dashboard_mode (PASS) | +| 2026-02-14 | BUG-T3 | Archived REVIEW_BUG-T3_webui_only_mode report | diff --git a/SPECS/INPROGRESS/next.md b/SPECS/INPROGRESS/next.md index 245009db..19e370b3 100644 --- a/SPECS/INPROGRESS/next.md +++ b/SPECS/INPROGRESS/next.md @@ -4,6 +4,7 @@ The previously selected task has been archived. ## Recently Archived +- 2026-02-14 — BUG-T3: Web UI cannot stay available when MCP bridge initialization fails (PASS) - 2026-02-14 — BUG-T2: codex mcp add with Web UI extras fails in zsh (PASS) - 2026-02-13 — FU-P9-T2-2: Add troubleshooting guidance for stale uvx cache/process versions (PASS) - 2026-02-13 — FU-P9-T4-1: Align publish_helper output with protected main branch workflow (PASS) diff --git a/SPECS/Workplan.md b/SPECS/Workplan.md index 28facaee..e6626cd8 100644 --- a/SPECS/Workplan.md +++ b/SPECS/Workplan.md @@ -58,6 +58,9 @@ Create a Python-based protocol compatibility wrapper that intercepts MCP respons ### Phase 12: Data Collection Enhancements **Intent:** Enrich collected telemetry with client identity, parameter patterns, and structured error classification for deeper operational insight. +### Phase 13: Persistent Broker & Shared Xcode Session +**Intent:** Introduce a long-lived broker connection to Xcode MCP so multiple short-lived clients can reuse one upstream session and reduce repeated Xcode permission prompts. + --- ## 3. Tasks @@ -1029,6 +1032,161 @@ Use any of the following forms so the extras spec is passed literally: --- +### BUG-T3: Web UI cannot stay available when MCP bridge initialization fails ✅ +- **Type:** Bug / Web UI / MCP Lifecycle +- **Status:** ✅ Complete +- **Priority:** P1 +- **Discovered:** 2026-02-14 +- **Completed:** 2026-02-14 +- **Component:** CLI startup and Web UI runtime +- **Affected Client:** Codex App / MCP users enabling `--web-ui` +- **Affected Surface:** Local dashboard (`http://localhost:8080`) + +#### Description +When users start the wrapper with `--web-ui` in MCP mode and the Xcode bridge handshake fails (or the MCP client disconnects early), the wrapper process exits before the dashboard remains usable. Users then see browser errors such as "Safari can't connect to the server" while trying to debug MCP connectivity. + +#### Symptoms +```text +MCP startup failed: handshaking with MCP server failed: connection closed: initialize response +Safari can't connect to the server +``` + +#### Root Cause Analysis +Web UI lifecycle is tightly coupled to MCP bridge lifecycle. If bridge startup/session fails, process shutdown also stops the Web UI, leaving no standalone dashboard mode for diagnosis. + +#### Workaround +Use standalone mode while diagnosing bridge issues: +- `xcodemcpwrapper --web-ui-only --web-ui-port 8080` +- `uvx --from 'mcpbridge-wrapper[webui]' mcpbridge-wrapper --web-ui-only --web-ui-port 8080` + +#### Resolution Path +- [x] Add a standalone `--web-ui-only` mode that starts dashboard services without launching `xcrun mcpbridge` +- [x] Ensure `--web-ui-only` implies Web UI enabled and respects `--web-ui-port` and config options +- [x] Add unit tests for arg parsing and main startup behavior in web-ui-only mode +- [x] Document when to use standalone dashboard mode for MCP connection troubleshooting + +--- + +### BUG-T4: Repeated Xcode permission prompts for each short-lived MCP client process +- **Type:** Bug / UX / Client Integration +- **Status:** 🔴 Open +- **Priority:** P1 +- **Discovered:** 2026-02-14 +- **Component:** MCP process lifecycle / Xcode authorization boundary +- **Affected Clients:** Codex CLI/App, other stdio MCP clients launching fresh processes +- **Affected Surface:** Xcode "agent wants to use Xcode's tools" prompt frequency + +#### Description +When MCP clients run in short-lived sessions, each session starts a new wrapper process (new PID). Xcode may request authorization again for each new process, creating repeated prompts and friction during normal usage. + +#### Symptoms +```text +The agent "Codex" ... PID 29854 wants to use Xcode's tools... +The agent "Codex" ... PID 30421 wants to use Xcode's tools... +``` + +#### Root Cause Analysis +Current integration is per-client `stdio`: each client launch creates a separate process and upstream bridge lifecycle. There is no persistent shared broker session to Xcode across clients. + +#### Workaround +Keep a single long-lived client/session running to reduce process churn. This is operationally fragile and does not solve multi-client workflows. + +#### Resolution Path +- [ ] Design persistent broker architecture for shared upstream Xcode session (P13-T1) +- [ ] Implement long-lived broker daemon with single upstream bridge connection (P13-T2) +- [ ] Add multi-client transport + stdio proxy mode to reuse broker session (P13-T3, P13-T4) +- [ ] Validate reduced prompt behavior and document rollout/migration steps (P13-T5, P13-T6) + +--- + +### BUG-T5: Empty-content tool results can still violate strict `structuredContent` contract +- **Type:** Bug / MCP Protocol Compliance +- **Status:** 🔴 Open +- **Priority:** P0 +- **Discovered:** 2026-02-14 +- **Component:** Response transformation engine +- **Affected Clients:** Strict MCP clients (Codex App/Cursor class behavior) + +#### Description +Some tool responses with `result.content: []` are currently passed through without adding `result.structuredContent`. For tools declaring output schema, strict clients may reject this as protocol-invalid. + +#### Symptoms +```text +Tool has output schema but did not return structured content +``` + +#### Root Cause Analysis +`needs_transformation()` intentionally skips empty content arrays, which can leave schema-required `structuredContent` absent for strict client validation paths. + +#### Workaround +Use clients/builds with compatibility fallback behavior. This is not reliable for strict validation paths. + +#### Resolution Path +- [ ] Implement FU-P13-T7 +- [ ] Add strict empty-content regression tests +- [ ] Verify behavior in Codex App and Codex CLI with same wrapper binary + +--- + +### BUG-T6: Web UI port collisions (`--web-ui-port`) create unstable multi-process behavior +- **Type:** Bug / Runtime / Process Lifecycle +- **Status:** 🔴 Open +- **Priority:** P0 +- **Discovered:** 2026-02-14 +- **Component:** CLI startup + Web UI runtime +- **Affected Surface:** Local dashboard and MCP startup reliability + +#### Description +Multiple stale/orphan wrapper instances can compete for the same Web UI port (for example `8080`), producing repeated bind failures and noisy startup state that complicates MCP diagnostics. + +#### Symptoms +```text +ERROR: [Errno 48] error while attempting to bind on address ('127.0.0.1', 8080): address already in use +``` + +#### Root Cause Analysis +Current startup does not enforce a single active Web UI instance per port nor provide deterministic collision recovery behavior. + +#### Workaround +Manually kill stale wrapper/uvx processes or use unique `--web-ui-port` values per client. + +#### Resolution Path +- [ ] Implement FU-P13-T8 +- [ ] Add deterministic collision handling tests +- [ ] Document stale-process cleanup in troubleshooting + +--- + +### BUG-T7: Unsupported `resources/*` methods can return non-standard error shape +- **Type:** Bug / MCP Compatibility / Error Normalization +- **Status:** 🔴 Open +- **Priority:** P0 +- **Discovered:** 2026-02-14 +- **Component:** Response normalization for non-tool methods +- **Affected Clients:** Clients expecting strict JSON-RPC error envelopes + +#### Description +For unsupported methods like `resources/list` and `resources/templates/list`, upstream may return tool-style `result.isError/content` payloads instead of JSON-RPC `error`. Some clients classify this as unexpected response type. + +#### Symptoms +```text +resources/list failed: Unexpected response type +resources/templates/list failed: Unexpected response type +``` + +#### Root Cause Analysis +Wrapper currently focuses on tool result `structuredContent` transformation and does not normalize unsupported non-tool method failures into canonical JSON-RPC `error` responses. + +#### Workaround +Ignore resource-listing failures when tool calls still work; behavior remains noisy and client-dependent. + +#### Resolution Path +- [ ] Implement FU-P13-T9 +- [ ] Add method-aware normalization regression tests +- [ ] Validate strict-client compatibility for `resources/*` probing + +--- + ### Phase 10: Web UI Control & Audit Dashboard **Intent:** Create a web-based dashboard for real-time monitoring, control, and audit logging of the XcodeMCPWrapper. Provides visibility into MCP tool usage, performance metrics, and operational control. @@ -1478,6 +1636,164 @@ Phase 9 Follow-up Backlog --- +### Phase 13: Persistent Broker & Shared Xcode Session + +**Intent:** Introduce a long-lived broker process that owns the Xcode bridge connection and multiplexes multiple MCP clients through one upstream session. + +#### P13-T1: Design persistent broker architecture and protocol contract +- **Description:** Define daemon lifecycle, client transport choice (Unix domain socket first), request/response correlation strategy, reconnect behavior, and failure boundaries between broker, upstream bridge, and client proxies. +- **Priority:** P0 +- **Dependencies:** P2-T6, P3-T10 +- **Parallelizable:** no +- **Outputs/Artifacts:** + - Broker architecture spec (sequence diagrams and lifecycle states) + - ADR documenting transport and security choices + - Initial module scaffold under `src/mcpbridge_wrapper/broker/` +- **Acceptance Criteria:** + - [ ] Architecture covers startup, shutdown, reconnect, and stale-socket recovery + - [ ] Correlation strategy for concurrent JSON-RPC requests is specified + - [ ] Security boundary for local clients is documented (socket permissions/token) + - [ ] Design is reviewed and approved for implementation + +--- + +#### P13-T2: Implement persistent broker daemon with single upstream Xcode bridge +- **Description:** Add daemon mode that launches and owns one `xcrun mcpbridge` subprocess, keeps it alive, and exposes broker readiness state to clients. +- **Priority:** P0 +- **Dependencies:** P13-T1 +- **Parallelizable:** no +- **Outputs/Artifacts:** + - `src/mcpbridge_wrapper/broker/daemon.py` + - PID/lock handling + stale lock cleanup + - Health endpoint or status command (`broker status`) +- **Acceptance Criteria:** + - [ ] Starting broker twice does not spawn duplicate upstream bridge instances + - [ ] Broker survives client disconnects without restarting upstream bridge + - [ ] Graceful shutdown terminates upstream process and cleans lock/socket files + - [ ] Crash recovery path is covered by tests + +--- + +#### P13-T3: Implement multi-client transport and JSON-RPC multiplexing +- **Description:** Add local transport server (Unix socket) that accepts multiple clients and multiplexes JSON-RPC traffic to/from the single upstream bridge while preserving per-client response routing. +- **Priority:** P0 +- **Dependencies:** P13-T2 +- **Parallelizable:** no +- **Outputs/Artifacts:** + - `src/mcpbridge_wrapper/broker/transport.py` + - Client session manager and request ID routing map + - Backpressure/queue limits and timeout handling +- **Acceptance Criteria:** + - [ ] At least two concurrent clients can perform tool calls successfully + - [ ] Responses are routed back to the correct client/request + - [ ] Broker handles malformed client payloads without affecting other clients + - [ ] Queue/timeout behavior is tested and deterministic + +--- + +#### P13-T4: Add stdio proxy mode for compatibility with existing MCP clients +- **Description:** Implement a proxy mode where standard MCP clients still use stdio, but the wrapper process forwards traffic to the persistent local broker instead of spawning a new upstream bridge. +- **Priority:** P1 +- **Dependencies:** P13-T3 +- **Parallelizable:** yes +- **Outputs/Artifacts:** + - CLI flags for broker usage (e.g., `--broker-connect`, `--broker-spawn`) + - Proxy adapter module under `src/mcpbridge_wrapper/broker/proxy.py` + - Backward-compatible default behavior toggle strategy +- **Acceptance Criteria:** + - [ ] Existing MCP client configs can opt into broker mode with minimal changes + - [ ] Proxy process exit does not terminate broker daemon + - [ ] Legacy direct mode remains available for fallback + - [ ] Unit tests cover proxy connect/disconnect and reconnect behavior + +--- + +#### P13-T5: Validate prompt reduction and multi-client stability +- **Description:** Add integration and manual verification that repeated short-lived client sessions can reuse the broker session without repeated upstream churn, plus load tests for concurrent calls. +- **Priority:** P1 +- **Dependencies:** P13-T4 +- **Parallelizable:** no +- **Outputs/Artifacts:** + - `tests/integration/test_broker_multi_client.py` + - Manual validation report for Xcode permission prompt behavior + - Metrics comparison (direct mode vs broker mode process churn) +- **Acceptance Criteria:** + - [ ] Sequential short-lived clients reuse one broker-owned upstream bridge process + - [ ] Concurrent client tool calls remain stable under load + - [ ] Manual test confirms no extra Xcode prompt while broker stays running + - [ ] Regression suite passes with broker mode enabled + +--- + +#### P13-T6: Document broker mode configuration, migration, and rollback +- **Description:** Update setup and troubleshooting docs for broker mode adoption, including client config examples, operational commands, limitations, and rollback to direct mode. +- **Priority:** P1 +- **Dependencies:** P13-T4 +- **Parallelizable:** yes +- **Outputs/Artifacts:** + - Updated `README.md`, `docs/cursor-setup.md`, `docs/claude-setup.md`, `docs/codex-setup.md`, `docs/troubleshooting.md` + - Optional `docs/broker-mode.md` deep-dive + - Config templates with broker-mode variants +- **Acceptance Criteria:** + - [ ] Docs include one-command start/stop/status flows for broker mode + - [ ] Client examples are provided for Codex/Cursor/Claude +- [ ] Troubleshooting includes socket/lock and stale-broker recovery +- [ ] Rollback steps to direct mode are explicit and tested + +--- + +#### FU-P13-T7: Enforce strict `structuredContent` compliance for empty-content tool results +- **Description:** Fix transformation logic so strict MCP clients no longer fail when a tool response includes `result.content: []` without `result.structuredContent`. Add a fallback injection strategy for transformable tool results with empty content. +- **Priority:** P0 +- **Dependencies:** P3-T3, P4-T1, P5-T6 +- **Parallelizable:** yes +- **Outputs/Artifacts:** + - Updated `src/mcpbridge_wrapper/transform.py` transformation conditions for empty-content results + - Updated `tests/unit/test_transform.py` coverage for strict empty-content compliance + - Updated troubleshooting/docs note clarifying strict-client behavior +- **Acceptance Criteria:** + - [ ] For tool responses missing `structuredContent`, empty `content` results are normalized to include `structuredContent` fallback + - [ ] Existing already-compliant responses remain unchanged + - [ ] Non-tool JSON-RPC notifications and unrelated payloads are not regressed + - [ ] New unit tests fail before fix and pass after fix + +--- + +#### FU-P13-T8: Prevent Web UI port collision from destabilizing MCP sessions +- **Description:** Harden startup behavior when `--web-ui` port is already occupied (common with stale/orphan wrapper processes). Ensure collision handling is deterministic and does not silently degrade MCP client stability. +- **Priority:** P0 +- **Dependencies:** P10-T1 +- **Parallelizable:** yes +- **Outputs/Artifacts:** + - Updated `src/mcpbridge_wrapper/__main__.py` Web UI startup collision handling + - Optional single-instance guard (lock/PID) for Web UI mode + - Tests for occupied-port startup behavior + - Troubleshooting updates for stale-process cleanup +- **Acceptance Criteria:** + - [ ] When requested Web UI port is occupied, wrapper behavior is explicit and deterministic (clear error or safe fallback) + - [ ] MCP stdio protocol output remains valid JSON-RPC only on stdout + - [ ] Repeated client startups no longer accumulate conflicting Web UI listeners on the same port + - [ ] Tests cover occupied-port and restart scenarios + +--- + +#### FU-P13-T9: Normalize unsupported `resources/*` method failures to standard JSON-RPC errors +- **Description:** Add protocol normalization for non-tool method failures where upstream returns tool-style `result.isError/content` payloads. Convert these into standard JSON-RPC `error` envelopes for strict MCP clients. +- **Priority:** P0 +- **Dependencies:** P3-T10 +- **Parallelizable:** yes +- **Outputs/Artifacts:** + - Updated response normalization logic in `src/mcpbridge_wrapper/__main__.py` and/or `src/mcpbridge_wrapper/transform.py` + - Request/response correlation support for method-aware normalization + - Regression tests for `resources/list` and `resources/templates/list` compatibility +- **Acceptance Criteria:** + - [ ] Unsupported non-tool methods return JSON-RPC `error` responses with stable code/message shape + - [ ] Codex/Cursor strict MCP paths no longer report "Unexpected response type" for normalized unsupported methods + - [ ] Tool-call success/error behavior remains backward compatible + - [ ] Integration tests cover normalization without false positives on valid tool results + +--- + ## 4. Dependency Graph ``` diff --git a/Sources/XcodeMCPWrapper/Documentation.docc/Troubleshooting.md b/Sources/XcodeMCPWrapper/Documentation.docc/Troubleshooting.md index d3dbb39c..f55f58f0 100644 --- a/Sources/XcodeMCPWrapper/Documentation.docc/Troubleshooting.md +++ b/Sources/XcodeMCPWrapper/Documentation.docc/Troubleshooting.md @@ -143,6 +143,28 @@ Then restart your MCP client. If you do not need Web UI, remove `--web-ui` and `--web-ui-port` from MCP config args. +## Error: "Safari can't connect to the server" after MCP startup failure + +**Symptom:** Browser cannot reach the dashboard URL (for example `http://localhost:8080`) after MCP startup fails with handshake/session errors. + +**Cause:** In normal MCP mode, Web UI runs in the same wrapper process as bridge startup. If bridge initialization fails or the MCP client disconnects early, the process exits and dashboard availability is lost. + +**Solution:** +Use standalone dashboard mode for diagnostics: + +```bash +# uvx +uvx --from 'mcpbridge-wrapper[webui]' mcpbridge-wrapper --web-ui-only --web-ui-port 8080 + +# manual install +xcodemcpwrapper --web-ui-only --web-ui-port 8080 + +# local development venv +/path/to/XcodeMCPWrapper/.venv/bin/mcpbridge-wrapper --web-ui-only --web-ui-port 8080 +``` + +`--web-ui-only` starts only the dashboard service and skips bridge startup. Use this mode to keep Web UI reachable while you debug MCP client connection issues separately. + ## Error: `zsh: no matches found: mcpbridge-wrapper[webui]` **Symptom:** Setup commands fail immediately in `zsh` before `uvx` runs. diff --git a/Sources/XcodeMCPWrapper/Documentation.docc/XcodeMCPWrapper.md b/Sources/XcodeMCPWrapper/Documentation.docc/XcodeMCPWrapper.md index 577cab28..3744e290 100644 --- a/Sources/XcodeMCPWrapper/Documentation.docc/XcodeMCPWrapper.md +++ b/Sources/XcodeMCPWrapper/Documentation.docc/XcodeMCPWrapper.md @@ -249,6 +249,17 @@ Features: Open http://localhost:8080 in your browser to view the dashboard. +## Known Issues + +- **BUG-T5 → FU-P13-T7 (P0):** Empty-content tool results can still violate strict `structuredContent` expectations in strict MCP clients. +- **BUG-T6 → FU-P13-T8 (P0):** Web UI port collisions can happen when multiple MCP sessions start with the same `--web-ui-port` (for example `8080`), producing `address already in use`. +- **BUG-T7 → FU-P13-T9 (P0):** `resources/list` and `resources/templates/list` probing may return non-standard error shapes in some client paths. +- **BUG-T3 (resolved):** If dashboard access is needed independently from MCP startup, run `--web-ui-only` for standalone diagnostics. + +### Disclaimer (Codex App) + +`mcpbridge-wrapper` normalizes Xcode MCP responses, but it does not control Codex App internals. Codex App transport/session behavior may change independently from Codex CLI and from this wrapper. If App and CLI differ, treat that as client-specific behavior first and verify with exact versions, config, and logs. + ## Tutorials - - Get up and running in minutes diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 06ee5931..916edcee 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -172,6 +172,28 @@ Then restart your MCP client. If you do not need Web UI, remove `--web-ui` and `--web-ui-port` from MCP config args. +### "Safari can't connect to the server" after MCP startup failure + +**Symptom:** Browser cannot reach the dashboard URL (for example `http://localhost:8080`) after MCP startup fails with handshake/session errors. + +**Cause:** In normal MCP mode, Web UI runs in the same wrapper process as bridge startup. If bridge initialization fails or the MCP client disconnects early, the process exits and dashboard availability is lost. + +**Solution:** +Use standalone dashboard mode for diagnostics: + +```bash +# uvx +uvx --from 'mcpbridge-wrapper[webui]' mcpbridge-wrapper --web-ui-only --web-ui-port 8080 + +# manual install +xcodemcpwrapper --web-ui-only --web-ui-port 8080 + +# local development venv +/path/to/XcodeMCPWrapper/.venv/bin/mcpbridge-wrapper --web-ui-only --web-ui-port 8080 +``` + +`--web-ui-only` starts only the dashboard service and skips bridge startup. Use this mode to keep Web UI reachable while you debug MCP client connection issues separately. + ### `zsh: no matches found: mcpbridge-wrapper[webui]` **Symptom:** Setup commands fail immediately in `zsh` before `uvx` runs. diff --git a/src/mcpbridge_wrapper/__main__.py b/src/mcpbridge_wrapper/__main__.py index 216b1130..bdc370e1 100644 --- a/src/mcpbridge_wrapper/__main__.py +++ b/src/mcpbridge_wrapper/__main__.py @@ -50,22 +50,31 @@ def _parse_webui_port(raw_value: str) -> int: return port -def _parse_webui_args(args: list) -> Tuple[bool, Optional[int], Optional[str], list]: +def _parse_webui_args( + args: list, +) -> Tuple[bool, bool, Optional[int], Optional[str], list]: """Parse web UI arguments from command-line args. - Extracts --web-ui, --web-ui-port, and --web-ui-config flags and + Extracts --web-ui, --web-ui-only, --web-ui-port, and --web-ui-config flags and returns them along with the remaining args to forward to the bridge. Args: args: Command-line arguments list. Returns: - Tuple of (web_ui_enabled, port_or_none, config_path_or_none, remaining_args). + Tuple of ( + web_ui_enabled, + web_ui_only_mode, + port_or_none, + config_path_or_none, + remaining_args, + ). Raises: ValueError: If --web-ui-port is not an integer in [1, 65535]. """ web_ui = False + web_ui_only = False port: Optional[int] = None config_path: Optional[str] = None remaining = [] @@ -75,6 +84,11 @@ def _parse_webui_args(args: list) -> Tuple[bool, Optional[int], Optional[str], l if args[i] == "--web-ui": web_ui = True i += 1 + elif args[i] == "--web-ui-only": + # Standalone dashboard mode (no bridge process). Implicitly enables Web UI. + web_ui = True + web_ui_only = True + i += 1 elif args[i] == "--web-ui-port" and i + 1 < len(args): port = _parse_webui_port(args[i + 1]) i += 2 @@ -91,7 +105,7 @@ def _parse_webui_args(args: list) -> Tuple[bool, Optional[int], Optional[str], l remaining.append(args[i]) i += 1 - return web_ui, port, config_path, remaining + return web_ui, web_ui_only, port, config_path, remaining def _extract_tool_name(line: str) -> Optional[str]: @@ -175,7 +189,9 @@ def main() -> int: # Parse web UI args from command line all_args = sys.argv[1:] if len(sys.argv) > 1 else [] try: - web_ui_enabled, web_ui_port, web_ui_config, bridge_args = _parse_webui_args(all_args) + web_ui_enabled, web_ui_only, web_ui_port, web_ui_config, bridge_args = _parse_webui_args( + all_args + ) except ValueError as exc: print(f"Error: {exc}", file=sys.stderr) return 2 @@ -188,7 +204,7 @@ def main() -> int: try: from mcpbridge_wrapper.webui.audit import AuditLogger from mcpbridge_wrapper.webui.config import WebUIConfig - from mcpbridge_wrapper.webui.server import run_server_in_thread + from mcpbridge_wrapper.webui.server import run_server, run_server_in_thread except ImportError: print( "Error: Web UI dependencies not installed. " @@ -212,6 +228,20 @@ def main() -> int: ) audit.enabled = config.audit_enabled + if web_ui_only: + print( + f"Web UI dashboard started at http://{config.host}:{config.port}", + file=sys.stderr, + ) + try: + # Standalone mode keeps only the dashboard process running. + run_server(config, metrics, audit) # type: ignore[arg-type] + except KeyboardInterrupt: + pass + finally: + audit.close() + return 0 + # metrics is SharedMetricsStore but server expects MetricsCollector # They have compatible interfaces for the Web UI read operations _ = run_server_in_thread(config, metrics, audit) # type: ignore[arg-type] diff --git a/tests/unit/test_main_webui.py b/tests/unit/test_main_webui.py index eb3626e3..9bcd7ae6 100644 --- a/tests/unit/test_main_webui.py +++ b/tests/unit/test_main_webui.py @@ -20,8 +20,9 @@ class TestParseWebUIArgs: def test_no_webui_args(self): """Test parsing with no web UI args.""" args = ["--some-other-arg"] - web_ui, port, config_path, remaining = _parse_webui_args(args) + web_ui, web_ui_only, port, config_path, remaining = _parse_webui_args(args) assert web_ui is False + assert web_ui_only is False assert port is None assert config_path is None assert remaining == ["--some-other-arg"] @@ -29,8 +30,9 @@ def test_no_webui_args(self): def test_webui_flag(self): """Test parsing --web-ui flag.""" args = ["--web-ui"] - web_ui, port, config_path, remaining = _parse_webui_args(args) + web_ui, web_ui_only, port, config_path, remaining = _parse_webui_args(args) assert web_ui is True + assert web_ui_only is False assert port is None assert config_path is None assert remaining == [] @@ -38,8 +40,9 @@ def test_webui_flag(self): def test_webui_port(self): """Test parsing --web-ui-port.""" args = ["--web-ui", "--web-ui-port", "9090"] - web_ui, port, config_path, remaining = _parse_webui_args(args) + web_ui, web_ui_only, port, config_path, remaining = _parse_webui_args(args) assert web_ui is True + assert web_ui_only is False assert port == 9090 assert config_path is None assert remaining == [] @@ -47,15 +50,17 @@ def test_webui_port(self): def test_webui_port_equals(self): """Test parsing --web-ui-port=9090.""" args = ["--web-ui", "--web-ui-port=9090"] - web_ui, port, config_path, remaining = _parse_webui_args(args) + web_ui, web_ui_only, port, config_path, remaining = _parse_webui_args(args) assert web_ui is True + assert web_ui_only is False assert port == 9090 def test_webui_config(self): """Test parsing --web-ui-config.""" args = ["--web-ui", "--web-ui-config", "/path/to/config.json"] - web_ui, port, config_path, remaining = _parse_webui_args(args) + web_ui, web_ui_only, port, config_path, remaining = _parse_webui_args(args) assert web_ui is True + assert web_ui_only is False assert port is None assert config_path == "/path/to/config.json" assert remaining == [] @@ -63,14 +68,16 @@ def test_webui_config(self): def test_webui_config_equals(self): """Test parsing --web-ui-config=/path.""" args = ["--web-ui", "--web-ui-config=/path/to/config.json"] - web_ui, port, config_path, remaining = _parse_webui_args(args) + web_ui, web_ui_only, port, config_path, remaining = _parse_webui_args(args) + assert web_ui_only is False assert config_path == "/path/to/config.json" def test_bridge_args_preserved(self): """Test that bridge args are preserved.""" args = ["--web-ui", "--web-ui-port", "9090", "--", "--bridge-arg"] - web_ui, port, config_path, remaining = _parse_webui_args(args) + web_ui, web_ui_only, port, config_path, remaining = _parse_webui_args(args) assert web_ui is True + assert web_ui_only is False assert port == 9090 assert remaining == ["--", "--bridge-arg"] @@ -84,12 +91,23 @@ def test_all_flags_together(self): "/config.json", "--bridge-arg", ] - web_ui, port, config_path, remaining = _parse_webui_args(args) + web_ui, web_ui_only, port, config_path, remaining = _parse_webui_args(args) assert web_ui is True + assert web_ui_only is False assert port == 9090 assert config_path == "/config.json" assert remaining == ["--bridge-arg"] + def test_webui_only_enables_webui(self): + """Test parsing --web-ui-only standalone mode.""" + args = ["--web-ui-only"] + web_ui, web_ui_only, port, config_path, remaining = _parse_webui_args(args) + assert web_ui is True + assert web_ui_only is True + assert port is None + assert config_path is None + assert remaining == [] + def test_webui_port_non_numeric_raises(self): """Test invalid non-numeric web UI port raises ValueError.""" with pytest.raises(ValueError, match="Invalid --web-ui-port value"): @@ -307,6 +325,38 @@ def test_main_with_webui_custom_port( write_calls = " ".join(str(c) for c in mock_stderr.write.call_args_list) assert ":9090" in write_calls + @patch("mcpbridge_wrapper.__main__.create_bridge") + def test_main_with_webui_only_skips_bridge(self, mock_create): + """Test standalone Web UI mode does not start bridge process.""" + pytest.importorskip("fastapi") + + with patch( + "mcpbridge_wrapper.__main__.sys.argv", + ["mcpbridge-wrapper", "--web-ui-only"], + ), patch("mcpbridge_wrapper.webui.server.run_server") as mock_run_server: + result = main() + + assert result == 0 + mock_run_server.assert_called_once() + mock_create.assert_not_called() + + @patch("mcpbridge_wrapper.__main__.create_bridge") + def test_main_with_webui_only_custom_port(self, mock_create): + """Test standalone Web UI mode honors custom port.""" + pytest.importorskip("fastapi") + + with patch( + "mcpbridge_wrapper.__main__.sys.argv", + ["mcpbridge-wrapper", "--web-ui-only", "--web-ui-port", "9091"], + ), patch("mcpbridge_wrapper.webui.server.run_server") as mock_run_server: + result = main() + + assert result == 0 + mock_create.assert_not_called() + args = mock_run_server.call_args[0] + config = args[0] + assert config.port == 9091 + @patch("mcpbridge_wrapper.__main__.create_bridge") def test_main_with_invalid_webui_port(self, mock_create): """Test main returns controlled error for invalid --web-ui-port."""