fix(mcp-server): mirror tool payloads into content so rows reach the model - #4295
Merged
Conversation
…e model Every list tool put its summary line in `content` and the rows only in `structuredContent`. Under MCP 2025-06-18 that field is contractually meaningful only when a tool declares an `outputSchema` — none of ours do — so a client may ignore it, and the spec asks servers returning structured content to also serialize it into a TextContent block. We did neither, so when the client stopped surfacing unschema'd structured content, every tool returned counts with no rows behind them. Adds a single `mirroredResult()` in `tools/output.ts` that `shapeListResult`, `shapeWriteResult` and `toToolErrorResult` all return through. No handler changed; all nineteen tools already funnel through those three functions. Also restores two less obvious things: the `memberBusinessId` values that `accounter_list_business_memberships` tells the model to pass, and the field-level `issues` on a `VALIDATION_ERROR`. The 60KB budget is unchanged and still measures what the model reads — `fittingCount` searches on the same string that is now mirrored. Guarded by `mirroring-contract.test.ts`: the suite previously passed green while the connector was blind, since rows were only ever asserted through `structuredContent`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gilgardosh
temporarily deployed
to
accounter-fullstack
August 26, 2026 14:04 — with
GitHub Actions
Inactive
gilgardosh
temporarily deployed
to
accounter-fullstack
August 26, 2026 14:04 — with
GitHub Actions
Inactive
Contributor
🚀 Snapshot Release (
|
| Package | Version | Info |
|---|---|---|
@accounter/client |
0.1.0-alpha-20260827074155-e9eda61bebae2bb5a0ed35f161555f186006f933 |
npm ↗︎ unpkg ↗︎ |
@accounter/green-invoice-graphql |
0.8.7-alpha-20260827074155-e9eda61bebae2bb5a0ed35f161555f186006f933 |
npm ↗︎ unpkg ↗︎ |
@accounter/hashavshevet-mesh |
0.2.13-alpha-20260827074155-e9eda61bebae2bb5a0ed35f161555f186006f933 |
npm ↗︎ unpkg ↗︎ |
@accounter/israeli-vat-scraper |
0.1.13-alpha-20260827074155-e9eda61bebae2bb5a0ed35f161555f186006f933 |
npm ↗︎ unpkg ↗︎ |
@accounter/modern-poalim-scraper |
0.11.0-alpha-20260827074155-e9eda61bebae2bb5a0ed35f161555f186006f933 |
npm ↗︎ unpkg ↗︎ |
@accounter/payper-mesh |
0.2.13-alpha-20260827074155-e9eda61bebae2bb5a0ed35f161555f186006f933 |
npm ↗︎ unpkg ↗︎ |
@accounter/scraper-app |
0.0.3-alpha-20260827074155-e9eda61bebae2bb5a0ed35f161555f186006f933 |
npm ↗︎ unpkg ↗︎ |
@accounter/server |
0.2.0-alpha-20260827074155-e9eda61bebae2bb5a0ed35f161555f186006f933 |
npm ↗︎ unpkg ↗︎ |
@accounter/shaam-uniform-format-generator |
0.2.7-alpha-20260827074155-e9eda61bebae2bb5a0ed35f161555f186006f933 |
npm ↗︎ unpkg ↗︎ |
@accounter/shaam6111-generator |
0.1.9-alpha-20260827074155-e9eda61bebae2bb5a0ed35f161555f186006f933 |
npm ↗︎ unpkg ↗︎ |
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
The fix is contained and well-tested, with only a minor suggested robustness tweak to one test heuristic.
Pull request overview
Fixes MCP tool output shaping so that structured payload rows (and error details) reliably reach the model by mirroring JSON into content alongside structuredContent, aligning with MCP 2025-06-18 expectations for unschema’d structured results.
Changes:
- Introduce
mirroredResult()and route list/write shaping (and taxonomy errors) through it so payloads are visible incontent. - Add targeted unit tests and a registry-wide contract test to prevent regressions where payloads are only emitted via
structuredContent. - Document the mirroring contract and rationale in the
mcp-serverREADME and publish a patch changeset.
File summaries
| File | Description |
|---|---|
| packages/mcp-server/src/tools/output.ts | Adds mirroredResult() and updates shaping helpers to mirror structured payload JSON into content. |
| packages/mcp-server/src/tools/tests/output.test.ts | Adds unit tests asserting list/write shapers mirror structured payloads into content and keep the byte budget meaningful. |
| packages/mcp-server/src/tools/tests/mirroring-contract.test.ts | Adds registry-wide tests enforcing that any structuredContent is also carried via a content text block. |
| packages/mcp-server/src/errors/taxonomy.ts | Mirrors error payloads into content so validation issues/correlationId reach the model. |
| packages/mcp-server/README.md | Documents the mirroring behavior and the contract test that enforces it. |
| .changeset/mcp-mirror-payload-into-content.md | Publishes a patch changeset describing the symptom, cause, fix, and test coverage. |
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…bstring The drift guard looked for the literal `content: [`, so `content:[` or `content : [` would have slipped through. Prettier normalizes the spacing today, but a guard against drift should not depend on prettier having run. Verified by injecting a `content : [` result into a tool file: the regex flags it, the substring check did not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gilgardosh
temporarily deployed
to
accounter-fullstack
August 27, 2026 07:37 — with
GitHub Actions
Inactive
gilgardosh
temporarily deployed
to
accounter-fullstack
August 27, 2026 07:37 — with
GitHub Actions
Inactive
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gilgardosh
temporarily deployed
to
accounter-fullstack
August 27, 2026 07:40 — with
GitHub Actions
Inactive
gilgardosh
temporarily deployed
to
accounter-fullstack
August 27, 2026 07:40 — with
GitHub Actions
Inactive
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.
The symptom
Asking the connector for a counterparty's charges returned this, and only this:
No ids, dates, amounts or business names — from any tool. The model couldn't work around it by
changing filters, because it had nothing to filter. Verified locally in Claude Desktop before and
after: full data now comes through.
Cause
Every list tool funnels through
shapeListResult, which put the summary incontentand the rowsonly in
structuredContent:Under MCP 2025-06-18,
structuredContentis contractually meaningful only when a tool advertises anoutputSchema. None of ours ever have —outputSchemahas never existed in this package. A clientis therefore free to ignore that field, and the spec correspondingly asks a server returning
structured content to also return it serialized in a
TextContentblock. We did neither, so everytool's data rode on undefined-by-spec client behaviour. When the client stopped surfacing unschema'd
structured content, the rows stopped arriving.
Not a regression in this repo.
shapeListResult's return statement was byte-identical to itsoriginal from the package's first commit (
b82802376);output.tshas been touched three timesever, and both recent changes were purely additive.
executeRegisteredTool(execute.ts:230) anddispatchMcpRequest(handler.ts:238) pass the result through untouched. It was an original designgap that only became visible when the assumption underneath it changed.
Confirmed empirically before fixing, by dumping a real result with a stubbed upstream:
Fix
A single
mirroredResult(summary, structured)insrc/tools/output.ts, whichshapeListResult,shapeWriteResultandtoToolErrorResultall return through. Summary still leads, followed by theserialized JSON;
structuredContentis kept as-is for hosts that consume it directly.Deliberately one function rather than a per-tool convention — the failure being fixed is exactly the
kind that drifts back one tool at a time. No tool handler changed; all nineteen already funnel
through those three functions.
Two less obvious things this restores:
accounter_list_business_membershipsinstructs the model to "Pass theirmemberBusinessIdvalues" while those ids lived in the invisible field. Discovery that can't be acted on breaks the
scoping workflow every other tool depends on.
VALIDATION_ERROR's field-levelissuesnever reached the model, soa rejected call said that it was wrong but never what to fix — and it would retry the same
shape.
accounter_explain_terminologywas likewise returning the whole glossary into a fieldnothing read.
The 60KB budget is unchanged and still measures what the model consumes:
fittingCountbinary-searches on
JSON.stringify(structured), which is now exactly the mirrored text. TheJSON-RPC body roughly doubles — far under the 1MB transport cap. A client rendering both channels
sees the payload twice; that's the accepted cost of not depending on which one it reads.
Why the tests didn't catch it
They couldn't. Rows were asserted exclusively through
result.structuredContent, andcontent[0].textonly ever for summary strings. The suite went 760 → 760 green across thebroken/fixed boundary.
tools/__tests__/mirroring-contract.test.tscloses that in two layers:toolRegistry.list()asserting anystructuredContentis carried by acontentblock. Verified it bites by temporarily reverting the fix — it fails per-tool with
accounter_search_charges returned structuredContent that no content block carries.src/tools/builds acontentarray by hand. The firstlayer has a blind spot: with an empty upstream, most data tools return a mirrored error, so a
new tool hand-rolling an unmirrored success could slip past it.
Verification
mcp-server— nothing outside imports these functionsFollow-up, deliberately not in this PR
Declaring
outputSchemaper tool would restorestructuredContentas a second channel and is theproper 2025-06-18 shape. It's left out because the fix no longer depends on it, and because
"servers MUST provide structured results that conform" means a schema drifting from the payload
turns working calls into client-side errors — 17 hand-written schemas of risk for redundancy we
don't currently need.
🤖 Generated with Claude Code