Skip to content

feat(mcp): surface response _meta on ToolCallOutputItem#3480

Open
zhang-liz wants to merge 7 commits into
openai:mainfrom
zhang-liz:feat/mcp-response-meta
Open

feat(mcp): surface response _meta on ToolCallOutputItem#3480
zhang-liz wants to merge 7 commits into
openai:mainfrom
zhang-liz:feat/mcp-response-meta

Conversation

@zhang-liz
Copy link
Copy Markdown

Summary

Capture the optional _meta field returned by MCP servers in CallToolResult and surface it on ToolCallOutputItem as mcp_response_meta. Applications can read auxiliary payloads (chart configs, trace IDs, frontend-only state) without forwarding them to the model.

This is the response-side complement to PR #2375 (tool_meta_resolver, request-side _meta). The field is a deep copy and is never injected into model context — only content / structuredContent are.

Implementation: invoke_mcp_tool stashes result.meta on the per-call ToolContext; the function-tool executor moves it onto the resulting ToolCallOutputItem. RunState schema bumped to 1.11 so the field round-trips through pause/resume.

Test plan

  • New unit tests in tests/mcp/test_mcp_util.py cover the ToolContext stash (populated, omitted, empty-dict; verifies deep copy).
  • New end-to-end tests in tests/mcp/test_runner_calls_mcp.py assert ToolCallOutputItem.mcp_response_meta is populated (streaming + non-streaming) and stays None when the server omits _meta.
  • New serialization round-trip tests in tests/test_run_state.py for schema 1.11.
  • make format, make lint clean. mypy clean on all touched files. Full make tests adds 8 new passing tests; only pre-existing unrelated failures (numpy/litellm/runloop optional deps) remain — verified identical to baseline (main shows the same 12 failed / 13 errors).

Issue number

Closes #3477

Checks

  • I've added new tests (if relevant)
  • I've added/updated the relevant documentation
  • I've run `make lint` and `make format`
  • I've made sure tests pass

zhang-liz added 7 commits May 20, 2026 23:04
Optional dict carrying CallToolResult._meta returned by MCP servers.
Appended at end of dataclass to preserve positional compatibility.
Populated by downstream commits; not yet wired here.
Per-call ToolContext gains _mcp_response_meta (private, not in __init__).
Used to thread server-returned _meta from invoke_mcp_tool to the
function-tool executor without changing the public invoker contract.
invoke_mcp_tool now deep-copies result.meta onto context._mcp_response_meta
when the server returned a non-empty dict and the caller passed a
ToolContext. Empty/missing _meta leaves the stash unset.
Function-tool executor reads tool_context._mcp_response_meta after
invocation, keys it by id(tool_run), and threads it into the
ToolCallOutputItem built in _build_function_tool_results.
Serialize ToolCallOutputItem.mcp_response_meta when non-empty and
restore it on deserialize. Empty dicts normalize to None.

Bumps CURRENT_SCHEMA_VERSION 1.10 -> 1.11 with matching
SCHEMA_VERSION_SUMMARIES entry. Extends the released-boundary test
to include 1.10 alongside the new unreleased writer.
- FakeMCPServer gains _response_meta to return _meta on CallToolResult.
- Unit tests assert invoke_mcp_tool deep-copies meta onto ToolContext,
  ignores empty/missing meta, and does not mutate when server-side
  dict changes after the call.
- End-to-end runner tests (streaming + non-streaming) assert the new
  field appears on ToolCallOutputItem; verify None when omitted.
New subsection under MCPServerStreamableHttp pairs with the existing
tool_meta_resolver docs and points at ToolCallOutputItem.mcp_response_meta
plus the streaming event shape.
@seratch
Copy link
Copy Markdown
Member

seratch commented May 21, 2026

Thanks for sharing this idea. However, I'd like to avoid this change because this is less flexible and it is a larger change than we'd like to have for this purpose.

@zhang-liz
Copy link
Copy Markdown
Author

Thanks for the quick review @seratch. Could you point me at the direction you'd prefer? I see two ways to slim this down:

  1. Callback-based: add an optional on_response_meta callback on the MCP server (mirroring tool_meta_resolver's shape). Server-returned _meta is delivered to user code with (tool_context, meta); no changes to ToolCallOutputItem, no schema bump, no executor plumbing.
  2. Context-only: stash _meta on ToolContext (private attr only) and let users read it from a custom RunHooks.on_tool_end hook. Zero new public surface.

Happy to rework as (1) or (2), or close this PR if you'd rather not have either. Want to make sure I'm aiming at the right shape before sending a v2.

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.

Read _meta from MCP Tool Call Responses

2 participants