Skip to content

fix(mcp): send content or structuredContent to the model, never both - #3234

Open
xpzouying wants to merge 7 commits into
MoonshotAI:mainfrom
xpzouying:fix/mcp-structured-content-fallback
Open

fix(mcp): send content or structuredContent to the model, never both#3234
xpzouying wants to merge 7 commits into
MoonshotAI:mainfrom
xpzouying:fix/mcp-structured-content-fallback

Conversation

@xpzouying

@xpzouying xpzouying commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Related Issue

Follow-up to #2596 (which resolved #2554), addressing user feedback that MCP tool results reached the model twice.

Problem

#2596 made the MCP structuredContent and _meta fields visible to the model by appending them as a serialized <mcp-result-extras> text block. But servers already render their data into content — as the spec's verbatim-JSON fallback (Google Workspace style) or as a human-readable reorganisation — so the model received the same information twice.

What changed

content and structuredContent become alternatives — never both forwarded:

  • content wins whenever it carries anything usable (a media block or non-whitespace text);
  • structuredContent fills in only when the content array is effectively empty (structuredContent-only servers keep working, preserving the fix(mcp): pass structuredContent and _meta through to model-visible tool output #2596 fix);
  • _meta (vendor-namespaced keys, protocol-reserved keys still filtered) is unchanged and always passes through.

Why not a smarter heuristic: there is no reliable signal that the structured payload is richer than what the server rendered into content — semantic equality misses faithful human reorganisations, and size ratios misjudge both directions. Rather than guess, the rule is absolute. The accepted trade-off: servers that put a lossy summary in content while keeping the real payload only in structuredContent will have that payload suppressed — such servers should render the data into content (the spec's SHOULD).

Applied identically to the v1 (packages/agent-core/src/mcp/output.ts) and v2 (packages/agent-core-v2/src/agent/mcp/output.ts) output pipelines.

Verification

  • Unit tests per server shape: verbatim dual-emit (incl. reformatted), faithful rendering, prose summary (suppressed by design), whitespace-only content (fallback fires), media-only (media counts as content), _meta filtering.
  • Round-trip suites in both packages spawn a real stdio MCP server (structured-content-stdio-server.mjs fixture) and push five result shapes through StdioMcpClienttoMcpToolResultmcpResultToExecutableOutput:
    • dual_emit / faithful_rendering / prose_plus_structured → model receives content only, no structured block;
    • structured_only → payload still surfaced;
    • meta_vendor → vendor _meta passes through.
  • Full suites green: agent-core 246 tests, agent-core-v2 240 tests; lint and typecheck clean.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue (external PRs: the issue must have a maintainer's /approve).
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

… no usable text

Servers that follow the MCP spec's backwards-compatibility SHOULD return
the same JSON both as a TextContent block and as structuredContent.
Forwarding both to the model sent the same data twice. structuredContent
now rides the mcp-structured-result block only when the content blocks
carry no usable text; _meta still always passes through.
@changeset-bot

changeset-bot Bot commented Aug 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c7814b9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Aug 25, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@c7814b9
npx https://pkg.pr.new/@moonshot-ai/kimi-code@c7814b9

commit: c7814b9

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 40bfd62987

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/agent-core/src/mcp/output.ts Outdated
);
const structuredExtras: Record<string, unknown> = {};
if (result.structuredContent !== undefined) {
if (result.structuredContent !== undefined && !hasUsableText) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve structured content unless the text is equivalent

Any non-empty text part suppresses structuredContent, even though it is not evidence that the text serializes the structured value. When an MCP server returns a human-readable status or summary such as Found 3 results together with structured fields containing the actual result records, this drops the only copy of those records before they reach the model; the v2 mirror has the same predicate. Compare against a serialization of the structured value, or retain structuredContent unless duplication can be established.

Useful? React with 👍 / 👎.

…erver

Round-trip four server result shapes (dual-emit, structuredContent-only,
prose+structured, vendor _meta) through StdioMcpClient and the output
pipeline, so the fallback behaviour is checked against real protocol
bytes instead of hand-built result objects.
…ation

The earlier has-usable-text gate also suppressed the structured payload
when content was a lossy human summary — the primary case from MoonshotAI#2554
(list_projects returning 'N item(s)' while the items live in
structuredContent). Skip the structured block only when a content text
block parses to the same JSON value (semantic compare, key order and
formatting insensitive); summaries and structured-only results still
pass through.
@xpzouying xpzouying changed the title fix(mcp): forward structuredContent only as fallback when content has usable text fix(mcp): dedupe structuredContent against its serialized content text block Aug 25, 2026
…y cover it

Replace the verbatim-serialization comparison with a size heuristic:
well-behaved servers render the same data into content (the spec's
dual-emit, or a faithful human reorganisation), and either way the text
measures at roughly the same size as the payload, so forwarding it would
double the information. Append the structured payload only when content
carries no usable text, or when the payload is more than twice the text
size — the signature of a lossy summary. Verified against a live video-
editor MCP whose tools all measure a json/text ratio of 1.2-1.9.
@xpzouying xpzouying changed the title fix(mcp): dedupe structuredContent against its serialized content text block fix(mcp): forward structuredContent only when content does not already cover it Aug 25, 2026
Final policy: content and structuredContent are alternatives. content
wins whenever it carries anything usable (a media block or non-whitespace
text); structuredContent fills in only for an empty content array. There
is no reliable signal that the structured payload is richer than what the
server already rendered into content, so no size or structure heuristic
is attempted. _meta still always passes through.
@xpzouying xpzouying changed the title fix(mcp): forward structuredContent only when content does not already cover it fix(mcp): send content or structuredContent to the model, never both Aug 25, 2026
The block carries structuredContent and/or _meta; the old
mcp-structured-result name was inaccurate whenever it is a pure _meta
carrier.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP tool results drop structuredContent — model only sees the summary text

1 participant