--- id: review-001-consumer-adapter-hygiene name: from_wss/from_mcp consumer fixes — plaintext ws://, 401 classification, token hygiene (CON-03, CON-05..CON-13) status: completed depends_on: [] scope: moderate risk: low impact: component level: implementation tags: [adapters, review-001, mcp, from-wss] --- ## Description Review 001 consumer-adapter findings, minus the hang fix (its own task — review-001-ws-robustness carries WS-02/CON-02), grouped as the small CON cleanup list: - **CON-01 (one-liner)**: `from_mcp/mod.rs:86` imports only the first `tools/list` page; `next_cursor` is never followed. rmcp 1.8 provides `list_all_tools()` for exactly this. Servers that paginate silently truncate the import — no error, no log. Fix and add a paginating-server test (COV gap 11). - **CON-03 (security)**: `from_wss.rs:114-128` accepts plaintext `ws://` and attaches the Bearer token unconditionally. A config typo silently ships a long-lived bearer credential over plaintext. Refuse `ws://` when a token is present — or unconditionally unless explicitly allowed — with a clear error. - **CON-04** (`from_mcp/mod.rs:240-248`): the audio variant of `content_block_union_schema` requires non-existent `"audio"` (property is `data`) — valid audio blocks never satisfy the published `output_schema`. Fix and validate a real audio block. - **CON-05/CON-07**: docs claim per-call credential reads; reality is import-time credentials (`from_mcp` transport-pinned token, `from_wss` bundles carry no capabilities at all). Correct docs, remove the dead capability read (`mod.rs:139-143`). - **CON-06** (`mod.rs:103-116`): 401 classified via `format!("{error:?}").contains("401")` — a `:4010/` port in a URL misclassifies as Unauthorized and real auth failures can be missed. Match typed variants; substring only as last resort. - **CON-11**: transport-level `tools/call` failures → undeclared `CallError::internal`; declare the failure mode (and preserve JSON-RPC error fidelity where cheap). - **CON-12**: remote tool names interpolated into op names unsanitized — a `/` in a tool name breaks the two-segment `ns/op` convention. Sanitize/validate at import. - **CON-13** (`from_wss.rs:48`, `from_mcp/mod.rs:37`): held tokens are plain `Option`; use `Secret` to match the crate's posture (no `Debug` derives exists — keep it that way). - **CON-10**: `[[test]] full_surface` is missing `test-support` in `required-features` → `cargo test --features mcp` fails to compile (empirically verified; masked by CI's `--all-features`). - **CON-08/CON-09**: no teardown path on `FromMCP` (each `import()` leaks an open server-side session + GET SSE stream) and `from_wss` import stacks duplicate sessions on reconnect by design (ADR-070 v1). Either add explicit close/teardown handles or write the ADR-070 note + module docs that make the limitation explicit — do not silently expand scope into a reconnect layer. ## Acceptance Criteria - [x] Paginated `tools/list` fully imported (paginating-server test) - [x] `ws://` + token refused (or explicit opt-out) with a clear error (test) - [x] Audio variant of `content_block_union_schema` satisfied by a valid block (test) - [x] No substring-based 401 classification on typed paths; a URL containing `:401x` no longer misclassifies (test) - [x] CON-05/07 docs corrected; dead capability read removed - [x] CON-11/12: transport-failure error declared; remote tool names sanitized (test) - [x] Tokens held as `Secret` - [x] `cargo test --features mcp` compiles and passes (CON-10), as does `cargo test --all-features` - [x] CON-08/09: close handles exist or docs/ADR-070 note state the limitation ## References - docs/reviews/001-initial-implementation-review.md (Part G, CON-03..CON-13) - docs/architecture/decisions/070-from-wss-consumer-adapter.md ## Notes > Independent of the WS tasks. > The from_wss notify/pending fixes are tracked separately in > review-001-ws-robustness (WS-02/CON-02 share one mechanism). > > Implementation notes: > - CON-03 chose the safer default: `ws://` is refused **whenever a token > is present** unless `FromWss::allow_plaintext()` is called explicitly > (unconditional refusal would break every existing test dial of a > token-less local producer; without a token there is no credential to > leak). `WssSession::connect` grew an `allow_plaintext` parameter; the > refusal lives in the config/validation path, before the dial. > - CON-06: typed matching on rmcp's `StreamableHttpError` > via `DynamicTransportError` downcast (`AuthRequired`, > `InsufficientScope`, `Client(e)` with `e.status() == 401`), with a > word-boundary string fallback (`AuthRequired` / `InsufficientScope` / > `www-authenticate` / the word "unauthorized") — a URL containing > `:40101` misclassifies no more (tested with a live 401 server and a > dead `:40101` endpoint). > - CON-13: alkcall already exports `Secret` (zeroizing, `[REDACTED]` > Debug) from `alkcall::core::types` — reused it for both adapters' > builder-held tokens; no new type. Debug-suppression test added. > - CON-11: transport-level `tools/call` failures declare > `MCP_TRANSPORT_ERROR` in each op's `error_schemas` (retryable); rmcp > `ServiceError::McpError` is preserved as `MCP_JRPC_` with the > message and `data` carried into the CallError (JSON-RPC fidelity). > - CON-12: sanitization is validate-and-refuse (not rewrite): a tool > name that is empty, contains `/` or whitespace fails the whole > import with `AdapterError::SchemaParse` — silently renames would > break the remote's own `tools/call` round-trip. > - CON-08/09: explicit-limitation notes, no close handles and no > reconnect layer — module docs on both adapters plus an "Explicit > session-lifetime limitation" section in ADR-070. ## Summary > Filled on completion. **Status: complete.** All eleven consumer-adapter findings from review 001 Part G are resolved; see the checkbox list plus the implementation notes above. - **CON-01**: `from_mcp` discovery now uses rmcp's `list_all_tools()` (follows `next_cursor`); integration test `import_follows_tools_list_pagination` exercises a three-page paginating server. - **CON-03**: `WssSession::connect` refuses `ws://` when a token is present unless `FromWss::allow_plaintext()` was called; clear `AdapterError::Transport` naming CON-03. Tests: refusal, explicit opt-in, and token-less passthrough. - **CON-04**: audio variant of `content_block_union_schema` now requires `["type", "data", "mimeType"]`; jsonschema-validated audio block test. - **CON-05**: dead `_token_present` capability read removed; module docs rewritten to state the import-time (transport-pinned) credential semantics. - **CON-06**: 401 classification is typed-first (downcast to rmcp's `StreamableHttpError`, matching `AuthRequired`, `InsufficientScope`, and `Client` errors with `status() == 401`); the string fallback checks auth wording, not bare "401". Tests cover a live 401, a dead `:40101` endpoint whose URL would substring-match, and the wording cases. - **CON-07**: `from_wss` docs now state the reality (dial-time token, imported handlers carry no capabilities at all). - **CON-08**: rmcp session teardown remains absent (no close handle); documented as an explicit limitation in the `from_mcp` module docs (import-once guidance, fire-and-forget leak named). - **CON-09**: same-disposition ADR-070 note: no teardown handle in v1, reconnect stacks duplicate sessions by design; no reconnect layer built. - **CON-10**: `[[test]] full_surface` `required-features` now `["mcp", "test-support"]`; `cargo test --features mcp` compiles and passes (verified). - **CON-11**: transport-level `tools/call` failures map to the newly declared `MCP_TRANSPORT_ERROR` error schema (retryable); rmcp JSON-RPC errors preserve code (`MCP_JRPC_`) and `data` (as error details). Integration test kills the server mid-call. - **CON-12**: `sanitize_tool_name` validates at import — `/` (which would break the two-segment `ns/op` convention), whitespace, and empty names fail the import with `SchemaParse`; unit + integration tests. - **CON-13**: both adapters hold tokens as alkcall's `Secret` (zeroizing on drop, `[REDACTED]` Debug); redaction is tested. Verification: `cargo test` (265 lib), `cargo test --features mcp` (313 lib + 9 from_mcp_integration — the CON-10 gate), `cargo test --all-features` (329 lib + all integration suites), `cargo clippy --all-targets -- -D warnings`, `cargo clippy --all-features --all-targets -- -D warnings`, `cargo fmt --check` — all pass.