feat(mcp): bound tool responses with queryable artifacts - #977
Open
LevSky22 wants to merge 26 commits into
Open
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
LevSky22
marked this pull request as ready for review
August 4, 2026 20:04
…ll handler invocation The standalone response-bounding branch's test harness predates the transport branch's SDK v2 migration. tools/call, prompts/get, and resources/read are unconditionally wrapped by _invokeInputRequiredCapableHandler, which reads ctx.mcpReq.requestState() before invoking the registered handler. Our own handler ignores ctx, but the wrapper still requires the accessor to exist.
Follow-up to the bounded-response work in this PR. Several paths returned reduced or empty results that were indistinguishable from correct ones, so a caller could state a confident conclusion from data that was not there. Correctness - A `filters[].path` that resolved on no item produced `[]` with `complete: true` and `warning: null` — byte-identical to a genuine zero-match. Since the pointer is relative to each item but the inline preview shows a different nesting, `/data/status` instead of `/status` is an easy mistake and read back as an authoritative "no rows". It now raises, naming the pointer and listing the item's real keys, and `response_meta.filters_applied` reports per-filter resolve/match counts so a real zero stays distinguishable from a wrong pointer. - `lt/lte/gt/gte` against operands that were never mutually comparable (a numeric field vs a quoted string) silently failed every item; that now raises too. - Projected `fields` that did not resolve became `null`, indistinguishable from a stored null. They are omitted instead, with `fields_resolved` counts. - `compact()` sliced arrays with no marker, so three items looked like the whole collection, and replaced scalars at depth with the string "[nested value omitted]", turning a count into text. Arrays now carry `_omitted_items` and scalars survive. - `boundToolResult` was applied to the artifact tools' own replies, re-artifacting them, cutting `text` to 1000 chars and nulling the top-level `next_cursor` — so a caller saw `null` and stopped paging after one partial page. Self-bounded tools are now exempt inside `boundToolResult`, and `read_response_artifact` sizes its page against the serialized envelope, since JSON escaping can inflate a 24 KiB window well past the budget. - Oversized non-array selections were compacted to a fraction of the value with `next_cursor: null` and no way to reach the rest. Objects now page by entry. - Budgets were measured on compact JSON while results are emitted with 2-space indent, understating real context cost. `serializeToolText` is now the single definition of that encoding, used by both the budget and the response formatter. - The inline preview is a reshaped summary, so pointers copied from it may not exist in the artifact. `response_meta.artifact.primary_paths` advertises pointers that do, and `query_response_artifact` gains `describe: true`, which reports keys, types and array lengths at a path so pointers do not have to be guessed. - Unknown, expired, wrong-scope and cursor/id-mismatch handles were all reported as "not found or has expired". They are now distinct, and a mismatch names the id the cursor was issued for, so a caller can recover instead of guessing. - `n8n_get_workflow` previews ran `connections` through `compact()`, reducing most edges to "[nested value omitted]" and dropping nodes past the 20th while still looking like a complete map. They now emit a full edge list. - `n8n_executions` reported `total_count` for what was only the current page; renamed to `page_count`. - `String(result)` in the stringify fallback produced the literal "[object Object]" with no `isError`, so the payload was lost silently. Replaced with a circular-safe serializer that flags the failure. Performance - The page-fitting loop dropped one element at a time, re-serializing the whole candidate page each step: ~100 serializations and ~148 MB stringified for a page of large items. Serialized size is monotonic in element count, so it bisects instead — 604 ms to 40 ms on a 39 MB artifact. - `query_response_artifact` re-read and re-parsed the entire artifact on every page. It now caches one parsed document, keyed by (id, mtime) with a TTL. - `boundToolResult` serialized the payload twice, once to test the budget and again to persist it; the buffer is passed through. - `pruneResponseArtifacts` ran a full readdir plus a stat per artifact on every persist. Now throttled, and skipped when a content-addressed id means no new file is written. - Artifact ids are content-addressed per scope, so an identical repeat request reuses one handle and refreshes its TTL instead of minting another copy. Artifact handles live on the container's writable layer, so the tool descriptions state retention as "valid until the MCP server restarts, and at most 24 hours" rather than implying they always survive 24 hours. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Conceived by Romuald Członkowski - www.aiadvisors.pl/en
LevSky22
force-pushed
the
feat/bounded-tool-responses-upstream
branch
from
August 14, 2026 03:58
ae33e64 to
ee55532
Compare
Conceived by Romuald Członkowski - www.aiadvisors.pl/en
Conceived by Romuald Członkowski - www.aiadvisors.pl/en
Conceived by Romuald Członkowski - www.aiadvisors.pl/en
tools_documentation({topic: 'query_response_artifact'}) answered "not found" for a
tool that works. The response-bounding layer registers its two wrappers in
src/services/mcp-response-bounding.ts, while the docs registry only ever imported
from src/mcp/tools.ts and tools-n8n-manager.ts, so both wrappers were callable and
undocumented. The overview compounded it: it claimed "24 Tools Total" while 27 are
registered, having also never counted tools_documentation itself.
- Add full docs entries for query_response_artifact and read_response_artifact,
leading with describe=true and primary_paths, since pointer drift off a reshaped
inline preview is the failure these tools actually produce.
- Add a Large-Result Tools section and a Documentation Tools line to the overview,
and set the header to 27. The category counts now sum to the header (1+1+1+2+2+18+2).
- Add tests/unit/mcp/tool-docs-coverage.test.ts, which imports the REAL registry.
The existing tools-documentation.test.ts mocks @/mcp/tool-docs, so it can check
the lookup logic but structurally cannot catch a coverage gap — which is why this
shipped. The new test walks every registered tool from all three layers and
asserts each is documented, that tools_documentation answers for it, that every
registry key matches its doc name, and that the declared total equals reality.
Sabotage-checked: commenting out one registry entry fails 2 tests. Unit suite 900
passed; build clean.
Conceived by Romuald Członkowski - www.aiadvisors.pl/en
Conceived by Romuald Członkowski - www.aiadvisors.pl/en
Conceived by Romuald Członkowski - www.aiadvisors.pl/en
Conceived by Romuald Członkowski - www.aiadvisors.pl/en
Conceived by Romuald Członkowski - www.aiadvisors.pl/en
Conceived by Romuald Członkowski - www.aiadvisors.pl/en
Conceived by Romuald Członkowski - www.aiadvisors.pl/en
Conceived by Romuald Członkowski - www.aiadvisors.pl/en
Conceived by Romuald Członkowski - www.aiadvisors.pl/en # Conflicts: # src/mcp/tools-documentation.ts
Conceived by Romuald Członkowski - www.aiadvisors.pl/en
Conceived by Romuald Członkowski - www.aiadvisors.pl/en
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
query_response_artifactfor model-facing artifact access; no raw payload reader is registeredstructuredContentunder an explicitoutputSchema/and suggest the exact corrected pointer when it resolvesArtifact query contract v3
query_response_artifactaccepts an RFC 6901responsePath, optional predicates, projectedfields,pageSize, an opaquecursor, boundedtextSearch, anddescribe.Inputs are strictly camelCase. Snake_case aliases are rejected by the tool schema, giving clients immediate validation feedback instead of allowing two competing input conventions.
Paths resolve exactly first and then, when applicable, relative to a previously selected response root. This keeps RFC 6901 behavior deterministic while supporting a path such as
/teamswhether the stored artifact contains the full response envelope or the already-selected collection.Projected root fields may use shorthand such as
id. Nested paths must be RFC 6901 pointers beginning with/; for example,/parameters/jsCode. A value such asparameters/jsCoderemains a literal root-key shorthand so keys containing/stay addressable. When that literal key is absent but the leading-slash pointer resolves, the error now suggests the exact correction rather than silently changing semantics.When the selected object contains exactly one array, projection can infer that collection. If every requested field redundantly includes the same collection prefix, the query interprets those pointers relative to each item only after exact projection fails, preserves the requested output labels, and reports the inferred path and normalization warning. Mixed or ambiguous prefixes still fail closed.
For keyed objects such as n8n
connections,objectMode: "entries"exposes{ key, value }rows. Incorrect array-style queries returnOBJECT_MODE_REQUIREDwith a bounded suggested request.describeis pageable, andtextSearchreturns bounded match contexts.Success and error results share the same structured envelope.
responseMetareports completeness, pagination, applied filters, field resolution, and artifact metadata. Recoverable failures include stable codes and bounded details so callers can correct paths, cursors, object modes, or expired handles without guessing.Context safety
Large payloads remain behind the query-only boundary. Tool calls return selected structure, matches, fields, or pages rather than raw artifact bytes. Optional MCP resource reads return only a bounded descriptor capped at 8 KiB, so enabling resource-link support does not reintroduce the stored provider response into model context.
Cursors are authenticated and bound to the artifact, caller scope, and exact query view. Existing compact tool results remain backward compatible; only oversized artifact access uses the v3 query contract.
Verification
npm run typechecknpm run buildConceived by Romuald Członkowski - www.aiadvisors.pl/en