Skip to content

Read _meta from MCP Tool Call Responses #3477

@MoothyWhite

Description

@MoothyWhite

Please read this first

  • Have you read the docs?Agents SDK docs
  • Have you searched for related issues? Others may have had similar requests

Feature Request: Read _meta from MCP Tool Call Responses

This is a follow-up to #2367 / PR #2375, which added support for sending _meta in MCP tool call requests.

This request is for the complementary feature: reading _meta from MCP tool call responses.

Problem

PR #2375 enabled passing _meta from the client (Agents SDK) to the MCP server via tool_meta_resolver. This is great for request tracing and context propagation.

However, the reverse direction is not supported: when an MCP server returns _meta in its CallToolResult, the Agents SDK currently ignores it entirely.

Looking at the source code (agents/mcp/util.py, invoke_mcp_tool, lines 666-689):

# Only these fields are read from the result:
if server.use_structured_content and result.structuredContent:
    tool_output = json.dumps(result.structuredContent)
else:
    for item in result.content:
        # ... handles text/image content

# result._meta is NEVER accessed

The MCP protocol spec defines CallToolResult._meta as:

interface CallToolResult {
  _meta?: { [key: string]: unknown };  // ← present in the spec, ignored by SDK
  content: ContentBlock[];
  structuredContent?: { [key: string]: unknown };
  isError?: boolean;
}

Our Use Case

We are building a data analysis assistant using the Agents SDK. The architecture involves:

  • A main Agent that handles user conversations
  • Sub-agents (as tools) that perform data analysis (SQL queries, chart generation)
  • An MCP server providing prompts and tools for database interaction

The Core Need: Dual-Output Tools

Our tools need to return two different outputs:

  1. For the LLM: A compact, token-efficient summary (e.g., "Query returned 1,245 rows")
  2. For the frontend: The full data payload (e.g., chart configuration JSON, raw data for rendering)

The _meta field in the MCP response is the natural place for this frontend-facing data:

{
  "content": [
    { "type": "text", "text": "Sales increased 23% in Q1." }
  ],
  "_meta": {
    "tool_meta": {
      "type": "chart",
      "vis_config": {
        "type": "line",
        "data": [{"month": "Jan", "sales": 120000}, ...]
      }
    }
  }
}

Why Not Put It in content?

If we put the full vis_config in result.content, it gets fed back to the LLM as context, wasting tokens on data the LLM doesn't need (and shouldn't see in full). The _meta field is explicitly defined in the MCP spec as metadata separate from the primary content.

Why Not Use RunContextWrapper?

RunContextWrapper works for local tools (via @function_tool), but our tools run in an MCP server process (stdio or HTTP). The MCP server cannot access the caller's RunContextWrapper because:

  • stdio MCP runs in a separate process
  • HTTP MCP runs on a remote host

The _meta field is the MCP-native mechanism for passing auxiliary data back to the client.

Current Workaround and Its Limitations

Our current workaround is to not use MCP for these tools at all — we register them as local FunctionTools so they can write to ctx.context. This means:

  • We cannot leverage MCP for tool distribution across services
  • We lose the MCP ecosystem benefits (standardized discovery, transport abstraction)
  • We have to maintain two separate tool registration paths (MCP for prompts, local for data tools)

Proposed Solution

Expose the response _meta through the SDK's tool execution pipeline. A few possible approaches:

Option A: Attach to ToolContext / RunContextWrapper

After invoke_mcp_tool receives the result, write result._meta into the current run context:

# In invoke_mcp_tool, after receiving result:
if result._meta and hasattr(context, 'context') and isinstance(context.context, dict):
    context.context.setdefault("_mcp_tool_meta", {})[tool.name] = result._meta

Option B: Attach to RunItem / ToolCallOutputItem

Extend ToolCallOutputItem (or the internal tool result representation) to carry _meta:

@dataclass
class ToolCallOutputItem:
    output: Any
    mcp_meta: dict[str, Any] | None = None  # ← new field

This would allow event stream consumers to access _meta via event.item.mcp_meta.

Option C: Return Tuple from invoke_mcp_tool

Change invoke_mcp_tool to return both the tool output and the meta:

# Current signature:
async def invoke_mcp_tool(...) -> ToolOutput

# Could be extended to return meta alongside:
async def invoke_mcp_tool(...) -> ToolOutput
# But meta is captured and stored somewhere accessible

Option B feels the cleanest as it aligns with the existing event stream architecture.

Related


Would the maintainers be open to a PR implementing Option B? We're happy to contribute.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions