fix(mcp): pass structuredContent and _meta through to model-visible tool output - #2596
Conversation
🦋 Changeset detectedLatest commit: 5387387 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
…ool output
MCP tool results were narrowed to {content, isError}, dropping the
spec-defined structuredContent field and _meta metadata. Servers that
return structured contracts in these fields (validated against
outputSchema, or namespaced metadata such as browser-handoff payloads)
were invisible to the agent. Surface them as a serialized
<mcp-structured-result> block appended to the tool output, still subject
to the existing text budget.
8f76ffb to
5387387
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5387387a42
ℹ️ 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".
| if (result._meta !== undefined) { | ||
| structuredExtras['_meta'] = result._meta; | ||
| } |
There was a problem hiding this comment.
Keep MCP result _meta out of model output
When an MCP Apps-compatible server returns _meta for component/client-only data, copying it into structuredExtras puts that side channel into the model-visible tool result and transcript; the tool-result docs define only content/structuredContent as model-visible and _meta as component-only (https://developers.openai.com/plugins/reference#tool-results). This can leak full record maps, trace IDs, or OAuth challenges that were intentionally omitted from content, so keep raw _meta off the model path or gate only narrowly reviewed keys.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fair point — partially adopted in #2600: _meta keys under protocol-reserved prefixes (a modelcontextprotocol/mcp label followed by another label, per the spec's key-name rules) are now filtered before serialization; those do carry host/protocol plumbing. Kept forwarding unprefixed and vendor-prefixed keys: "_meta is component-only" is an OpenAI Apps convention rather than MCP spec semantics — the spec leaves non-reserved namespaces to the server, and the host can't know which of them the model is meant to see without hard-coding vendor knowledge.
| } | ||
|
|
||
| const wrapped = wrapMediaOnly(converted, qualifiedToolName); | ||
| // Structured payloads (structuredContent per MCP spec, plus server metadata |
There was a problem hiding this comment.
Move the v2 implementation comment to the header
Under packages/agent-core-v2, implementation comments are supposed to be confined to the top-of-file /** */ role block; this new explanatory block sits inside mcpResultToExecutableOutput, so it violates the scoped convention. Please move any durable module-level rationale to the file header or drop the inline narration.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in #2600 — commentary moved to the module header per the agent-core-v2 convention.
…#2600) * fix(mcp): drop protocol-reserved _meta keys from model-visible output Follow-up to #2596. The MCP spec reserves _meta key prefixes whose labels include "modelcontextprotocol" or "mcp" for protocol use; those entries carry host/protocol plumbing rather than model-facing data, so filter them out before serializing the <mcp-structured-result> block. Unprefixed and vendor-prefixed keys still pass through — their semantics belong to the server. Also moves the v2 implementation commentary into the module header per the agent-core-v2 comment convention. * fix(mcp): reserve _meta prefixes only when a label follows mcp/modelcontextprotocol Per the spec's key-name rules a prefix is reserved when a modelcontextprotocol or mcp label is followed by at least one more label; a trailing reserved word (com.example.mcp/) is a legitimate vendor namespace and now passes through. --------- Co-authored-by: zouying <zouying@moonshot.cn>
…ool output (MoonshotAI#2596) MCP tool results were narrowed to {content, isError}, dropping the spec-defined structuredContent field and _meta metadata. Servers that return structured contracts in these fields (validated against outputSchema, or namespaced metadata such as browser-handoff payloads) were invisible to the agent. Surface them as a serialized <mcp-structured-result> block appended to the tool output, still subject to the existing text budget. Co-authored-by: zouying <zouying@moonshot.cn>
…MoonshotAI#2600) * fix(mcp): drop protocol-reserved _meta keys from model-visible output Follow-up to MoonshotAI#2596. The MCP spec reserves _meta key prefixes whose labels include "modelcontextprotocol" or "mcp" for protocol use; those entries carry host/protocol plumbing rather than model-facing data, so filter them out before serializing the <mcp-structured-result> block. Unprefixed and vendor-prefixed keys still pass through — their semantics belong to the server. Also moves the v2 implementation commentary into the module header per the agent-core-v2 comment convention. * fix(mcp): reserve _meta prefixes only when a label follows mcp/modelcontextprotocol Per the spec's key-name rules a prefix is reserved when a modelcontextprotocol or mcp label is followed by at least one more label; a trailing reserved word (com.example.mcp/) is a legitimate vendor namespace and now passes through. --------- Co-authored-by: zouying <zouying@moonshot.cn>
…ool output (MoonshotAI#2596) MCP tool results were narrowed to {content, isError}, dropping the spec-defined structuredContent field and _meta metadata. Servers that return structured contracts in these fields (validated against outputSchema, or namespaced metadata such as browser-handoff payloads) were invisible to the agent. Surface them as a serialized <mcp-structured-result> block appended to the tool output, still subject to the existing text budget. Co-authored-by: zouying <zouying@moonshot.cn>
…MoonshotAI#2600) * fix(mcp): drop protocol-reserved _meta keys from model-visible output Follow-up to MoonshotAI#2596. The MCP spec reserves _meta key prefixes whose labels include "modelcontextprotocol" or "mcp" for protocol use; those entries carry host/protocol plumbing rather than model-facing data, so filter them out before serializing the <mcp-structured-result> block. Unprefixed and vendor-prefixed keys still pass through — their semantics belong to the server. Also moves the v2 implementation commentary into the module header per the agent-core-v2 comment convention. * fix(mcp): reserve _meta prefixes only when a label follows mcp/modelcontextprotocol Per the spec's key-name rules a prefix is reserved when a modelcontextprotocol or mcp label is followed by at least one more label; a trailing reserved word (com.example.mcp/) is a legitimate vendor namespace and now passes through. --------- Co-authored-by: zouying <zouying@moonshot.cn>
Related Issue
No linked issue — the problem is explained below.
Problem
The MCP
tools/callresult shape includesstructuredContent(spec-defined, validated against a tool'soutputSchema) and_meta(namespaced server metadata). kimi-code's client narrows results to{content, isError}intoMcpToolResult, so anything a server returns in these fields is silently dropped before the agent loop ever sees it.Servers that put their machine-readable contract in
structuredContent— or carry side-channel payloads such as browser-handoff URLs in_meta— appear to the model as if they returned only their prose blocks. Other MCP hosts (Claude Code, Codex) pass this data through to the model, so servers written against those hosts regress under kimi-code.What changed
MCPToolResultgains optionalstructuredContent?: unknownand_meta?: Record<string, unknown>(v1packages/agent-core/src/mcp/types.ts, v2packages/agent-core-v2/src/mcpCore/types.ts).toMcpToolResult(v1mcp/client-shared.ts, v2mcpCore/client-shared.ts) preserves both fields when present instead of discarding them.mcpResultToExecutableOutput(v1mcp/output.ts, v2agent/mcp/output.ts) serializes them into a trailing<mcp-structured-result>{…}</mcp-structured-result>text part. Ordering and hardening:<mcp_tool_result name="…">attribution;</mcp-structured-result>sequences inside the serialized payload are stripped, so server-controlled data cannot fake an early end of the block;This mirrors the existing in-tree convention of tagging machine-attributable tool output with a distinctive text wrapper (
<mcp_tool_result>for media-only results); the tag carries no host-side semantics — nothing parses it back — it only lets the model attribute the JSON as a structured payload rather than prose.Verification
packages/agent-core/test/mcp/output.test.tsand v2packages/agent-core-v2/test/agent/mcp/output.test.ts: passthrough serialization, media-only attribution preserved, closing-tag stripping.tsc --noEmitpasses foragent-coreandagent-core-v2.@moonshot-ai/kimi-code: patch).