feat(mcp): bound tool responses with pageable artifacts - #977
Open
LevSky22 wants to merge 12 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
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
response_meta.artifactquery_response_artifactfor generic JSON selection, filtering, projection, shape discovery, and semantic paginationread_response_artifactas a raw 24 KiB page fallbackn8n_executions({ action: "list" })default from 100 to 20 while preserving explicit pagination up to 100Response artifact contract v2
query_response_artifactaccepts an RFC 6901responsePath, optional generic predicates, projected fields, a page size, and an opaque cursor. Projected fields accept root names such asidor pointers such as/status/name. Missing fields are omitted and reported throughfields_resolved; an entirely missing projection is rejected.For keyed objects such as native n8n
connectionsmaps,objectMode: "entries"exposes{key, value}rows. Callers can filter/keywithinand retrieve several source nodes in one request instead of guessing nested array paths.describe: trueis pageable. Object keys include absolute RFC 6901 pointers; array item keys remain relative to each item for direct use in fields and filters. Query and raw-read results identify contract version 2 and do not repeat the full artifact descriptor minted by the originating tool.Cursors are HMAC authenticated, versioned, and bound to the artifact, instance scope, and exact query view. One oversized item is summarized while still advancing the cursor.
Limits and metadata
A compact preview is explicitly incomplete while
response_meta.completeis false, regardless of upstream fields such aslast_page. Artifact IDs remain instance-scoped, validated, atomically written with private permissions, and inaccessible across scopes.This follows MCP's opaque state-handle guidance while using a custom cursor inside the tool for payload pagination; protocol-level pagination does not cover arbitrary tool result bodies.
Deployment behavior
Artifacts use container-local
/tmp/n8n-mcp-artifactsby default. Persistence across container restarts is optional throughMCP_RESPONSE_ARTIFACT_ROOTand a stableMCP_RESPONSE_CURSOR_KEY.Upstream compatibility retained
n8n_get_workflowstill defaults tofull, strips the duplicate active-version payload, and retainsanthropic/maxResultSizeCharsas defense in depth.summary/filtered/fullsemantics and the existing 1,000-item explicit filtered-mode cap are unchanged.Verification
npm run typechecknpm run buildtest,cjs-runtime,secretlint, andpublish-resultsjobs pass on commit7304103