- search honors the optional query substring filter (PRJ-07)
- search excludes both Sub andPub ops per ADR-041/ADR-068; dead match
arms removed (PRJ-08)
- batch returns {"results": [...]} with {isError, output|error} items;
tool description states the shape (PRJ-09)
- structuredContent is always an object: non-object outputs wrapped as
{"result": <output>} (PRJ-10)
- argument errors are CallError values (retryable always present);
non-string operation no longer reports 'missing required field'
(PRJ-13)
Verified: cargo test --all-features, cargo test, clippy -D warnings,
fmt --check
97 lines
4.5 KiB
Markdown
97 lines
4.5 KiB
Markdown
---
|
|
id: review-001-mcp-tool-fidelity
|
|
name: MCP tool-gateway runtime fidelity (PRJ-07, PRJ-08, PRJ-09, PRJ-10, PRJ-13)
|
|
status: completed
|
|
depends_on: [review-001-schema-internal-visibility]
|
|
scope: narrow
|
|
risk: low
|
|
impact: component
|
|
level: implementation
|
|
tags: [adapters, review-001, mcp]
|
|
---
|
|
|
|
## Description
|
|
|
|
Review 001 MCP projection findings in `src/adapters/to_mcp.rs` (the
|
|
schema-ACL half of the MCP story is review-001-schema-internal-visibility;
|
|
this task covers the rest of the tool surface):
|
|
|
|
- **PRJ-07**: the `search` tool advertises an optional substring `query`
|
|
filter but `call_tool` drops `arguments` entirely (`:382`) — an LLM
|
|
passing `{"query": "fs"}` gets the full unfiltered listing (context
|
|
waste/overflow in big registries). Honor the filter.
|
|
- **PRJ-08**: `search` excludes only `Sub`, not `Pub` (`:268-274`),
|
|
contradicting ADR-068 ("excludes both"). A discovered Pub op can never
|
|
be invoked via the `call` tool — the advertised discovery surface is a
|
|
lie for the whole Pub class. (The `"subscription"`/`"Sub"` match arms
|
|
are dead — `op_type_str` only emits lowercase.)
|
|
- **PRJ-09**: batch item shape `{"isError", "output"|"error"}` contradicts
|
|
the tool description ("each shaped like a `call` result") — fix the
|
|
description or the shape.
|
|
- **PRJ-10**: `structuredContent` passes output through verbatim, so
|
|
string/array/null outputs produce non-object `structuredContent`
|
|
(strict MCP clients may reject; batch returns a top-level array). Wrap
|
|
or document.
|
|
- **PRJ-13**: hand-rolled argument errors omit `retryable` (required by
|
|
the OpenAPI error schemas, always present on `CallError`), and a
|
|
non-string `operation` reports the misleading "missing required field".
|
|
Build argument errors in the `CallError` wire shape.
|
|
|
|
## Acceptance Criteria
|
|
|
|
- [x] `search` respects a `query` argument (test); `Pub` ops excluded from results (test)
|
|
- [x] Batch item doc matches the emitted shape (or shape changed to match — one way or the other, tested)
|
|
- [x] Non-object outputs wrapped into objects or documented as an exception; batch shape consistent
|
|
- [x] MCP argument errors carry `retryable` and truthful messages
|
|
- [x] `cargo test --all-features` passes
|
|
|
|
## References
|
|
|
|
- docs/reviews/001-initial-implementation-review.md (Part F, PRJ-07..PRJ-10, PRJ-13)
|
|
- docs/architecture/decisions/041-mcp-tool-gateway-pattern.md
|
|
- docs/architecture/decisions/068-gateway-publish-endpoint.md
|
|
|
|
## Notes
|
|
|
|
> Agent fills during implementation. Depends on the visibility task
|
|
> because both rework the `schema` tool in `to_mcp.rs`.
|
|
|
|
## Summary
|
|
|
|
All five findings fixed in `src/adapters/to_mcp.rs`:
|
|
|
|
- **PRJ-07**: `call_tool` extracts the optional `query` argument and
|
|
`handle_search` filters the listing by substring match on the
|
|
operation name (after the visibility/ACL filter — the query narrows
|
|
what the caller is allowed to see, never widens it).
|
|
- **PRJ-08**: exclusion now covers both `"sub"` and `"pub"` (ADR-068 §1
|
|
and ADR-041 §2); the dead `"subscription"`/`"Sub"` match arms are
|
|
removed. A `search_excludes_pub_ops` test asserts the Pub op is
|
|
invisible to MCP discovery.
|
|
- **PRJ-09**: shape decided over doc — `batch` now returns
|
|
`{"results": [...]}` where each entry is
|
|
`{"isError": bool, "output": ...}` on success or
|
|
`{"isError": true, "error": <CallError>}` on failure; the tool
|
|
description states exactly that (multiple dispatch-path failures in
|
|
one batch let the other items run to completion, so the old
|
|
"shaped like a `call` result" description was the wrong side to
|
|
keep).
|
|
- **PRJ-10**: module doc documents the object guarantee: object outputs
|
|
pass through verbatim, non-object outputs are wrapped as
|
|
`{"result": <output>}` (`object_result`), applied consistently to
|
|
`call`'s `structuredContent` and to batch item `output` — so every
|
|
`structuredContent` is a JSON object and batch is never a top-level
|
|
array.
|
|
- **PRJ-13**: all argument errors (`call`, `batch`, `schema`, and the
|
|
batch per-item errors) are `CallError` values serialized through the
|
|
same `call_error_to_structured_error` path, so `retryable` is always
|
|
present. `parse_call_arguments` now distinguishes a missing
|
|
`operation` ("missing required field") from a non-string one
|
|
("must be a string, got <type>").
|
|
|
|
Tests: `search_honors_query_substring_filter`,
|
|
`search_excludes_pub_ops`, `call_wraps_non_object_output_into_object_
|
|
structured_content`, `call_argument_errors_carry_retryable_and_truthy_
|
|
messages`, `batch_returns_object_with_result_entries` (retryable +
|
|
INVALID_INPUT per-item check); existing tests updated to the new
|
|
shapes. |