Skip to content

[BUG] Strands MCPClient does not forward _meta to MCP tool calls + instrumentation corrupts _meta #1916

@mananpatel320

Description

@mananpatel320

Checks

  • I have updated to the lastest minor and patch version of Strands
  • I have checked the documentation and this is not expected behavior
  • I have searched ./issues and there are no duplicates of my issue

Strands Version

1.30.0

Python Version

3.11.5

Operating System

macOS Tahoe 26.1

Installation Method

pip

Steps to Reproduce

Summary

Two related issues prevent _meta (per MCP spec) from reaching MCP servers when using Strands:

  1. MCPClient never passes meta to ClientSession.call_tool() — there is no way for users to attach custom _meta to tool calls.
  2. mcp_instrumentation.py corrupts _meta — it uses model_dump() instead of model_dump(by_alias=True), sending "meta" instead of "_meta" on the wire.

Steps to Reproduce

Part 1: _meta not forwarded

MCPClient._create_call_tool_coroutine calls:

# strands/tools/mcp/mcp_client.py
return await cast(ClientSession, self._background_thread_session).call_tool(
    name, arguments, read_timeout_seconds
)

But ClientSession.call_tool supports a meta kwarg:

# mcp/client/session.py
async def call_tool(
    self,
    name: str,
    arguments: dict[str, Any] | None = None,
    read_timeout_seconds: timedelta | None = None,
    progress_callback: ProgressFnT | None = None,
    *,
    meta: dict[str, Any] | None = None,  # <-- never passed by Strands
) -> types.CallToolResult:

The meta kwarg is never forwarded, so custom _meta never reaches the server.

Part 2: Instrumentation corrupts _meta

In strands/tools/mcp/mcp_instrumentation.py, the patch_mcp_client function does:

params_dict = request.root.params.model_dump()          # BUG: missing by_alias=True
meta = params_dict.setdefault("_meta", {})               # creates NEW key since "meta" != "_meta"

The MCP Pydantic model defines:

class CallToolRequestParams:
    meta: Meta | None = Field(alias="_meta")  # Python name is "meta", wire name is "_meta"

So model_dump() returns {"meta": {...}} (Python field name), not {"_meta": {...}} (wire format). Then setdefault("_meta", {}) finds no "_meta" key and creates a new empty one, resulting in:

{
  "meta": {"progressToken": null, "com.example/request_id": "abc-123"},
  "_meta": {}
}

Proof

from mcp.types import CallToolRequestParams

params = CallToolRequestParams(
    name="echo",
    arguments={"message": "hello"},
    _meta={"com.example/request_id": "abc-123"},
)

# What strands instrumentation does:
print(params.model_dump())
# {'meta': {'progressToken': None, 'com.example/request_id': 'abc-123'}, ...}
#  ^ Python field name "meta", NOT "_meta"

# What it should do:
print(params.model_dump(by_alias=True))
# {'_meta': {'progressToken': None, 'com.example/request_id': 'abc-123'}, ...}
#  ^ Correct wire format "_meta"

Expected Behavior

Expected Behavior

  1. MCPClient should expose a way to pass custom _meta through to ClientSession.call_tool(meta=...).
  2. mcp_instrumentation.py should use model_dump(by_alias=True) so _meta is serialized correctly.

Actual Behavior

  1. _meta is always None on the MCP server — When using Strands MCPClient to call MCP tools, the server
    receives _meta: None regardless of any custom metadata the user intends to send. There is no API
    surface on MCPClient to pass _meta through to ClientSession.call_tool().

  2. When OpenTelemetry instrumentation is active, _meta is corrupted on the wire — The outbound JSON-RPC
    payload contains both "meta" (with the original data) and "_meta" (empty {}), instead of a single
    "_meta" with the correct values:

What gets sent on the wire:

"params": {
"meta": {"progressToken": null, "com.example/request_id": "abc-123"},
"_meta": {}
}

What should be sent:

"params": {
"_meta": {"progressToken": null, "com.example/request_id": "abc-123"}
}

Additional Context

No response

Possible Solution

Suggested Fix

Part 1 — Add meta parameter to call_tool_sync/call_tool_async and forward it:

# In _create_call_tool_coroutine
async def _call_tool_direct() -> MCPCallToolResult:
    return await cast(ClientSession, self._background_thread_session).call_tool(
        name, arguments, read_timeout_seconds, meta=meta  # forward meta
    )

Part 2 — One-line fix in mcp_instrumentation.py:

# Before (buggy)
params_dict = request.root.params.model_dump()

# After (fixed)
params_dict = request.root.params.model_dump(by_alias=True)

Related Issues

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions