- CON-01: from_mcp discovery follows tools/list pagination (rmcp list_all_tools); three-page paginating-server test - CON-03: from_wss refuses ws:// with a Bearer token unless FromWss::allow_plaintext() is called explicitly (tests: refusal, opt-in, token-less passthrough) - CON-04: audio variant of content_block_union_schema requires ["type","data","mimeType"]; jsonschema-validated audio block - CON-05/07: import-time credential documented on both adapters; dead per-call capability read removed - CON-06: 401 classification typed-first (downcast to rmcp StreamableHttpError<reqwest::Error>; AuthRequired/InsufficientScope/ Client with status 401); a :40101 URL no longer misclassifies (tested) - CON-11: transport tools/call failures declare MCP_TRANSPORT_ERROR; rmcp JSON-RPC errors preserve code (MCP_JRPC_<code>) and data - CON-12: tool names validated at import (/, whitespace, empty → SchemaParse); unit + integration tests - CON-13: tokens held as alkcall Secret<String> (zeroize, redacted Debug) - CON-08/09: no close handles; explicit-limitation notes in from_mcp module docs, from_wss module docs, and ADR-070 - CON-10: full_surface [[test]] required-features = ["mcp","test-support"]; cargo test --features mcp now compiles and passes Verified: cargo test; cargo test --features mcp; cargo test --all-features; cargo clippy (--all-features) --all-targets -- -D warnings; cargo fmt --check
164 lines
8.5 KiB
Markdown
164 lines
8.5 KiB
Markdown
---
|
|
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<String>`; use `Secret<String>` 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<String>`
|
|
- [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<reqwest::Error>`
|
|
> 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<T>` (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_<rrrr0>` 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<reqwest::Error>`, 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_<code>`) 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<String>`
|
|
(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. |