Description
JSONRPCDispatcher.send_raw_request attaches _meta to params unconditionally:
out_params = dict(params) if params is not None else {}
out_meta = dict(out_params.get("_meta") or {})
on_progress = opts.get("on_progress")
if on_progress is not None:
out_meta["progressToken"] = request_id
out_params["_meta"] = out_meta # <- empty dict goes on the wire
When no progress callback is set and the otel tracer is a no-op (default), out_meta stays {} and every request ships _meta: {}.
Strict JSON-RPC servers reject this: Meta's hosted Ads MCP server (https://mcp.facebook.com/ads) returns HTTP 400 with -32602 "_meta for Request must be a dict or null" on initialize, making it unreachable from any client built on this SDK. _meta: null is rejected identically — the field must be absent.
Other MCP clients Meta documents (Claude Code, ChatGPT) don't emit an empty _meta and connect fine.
Reproduction
Reported downstream from Hermes Agent (NousResearch/hermes-agent#105637), with a minimal curl repro against the real server (400 with _meta:{}}, 200 without). Happy to attach a self-contained repro against a local strict server if preferred.
Proposed fix
if out_meta:
out_params["_meta"] = out_meta
elif "_meta" in out_params:
del out_params["_meta"]
after inject_trace_context(out_meta) so a real trace context is still sent.
Environment
- mcp SDK: current main (
src/mcp/shared/jsonrpc_dispatcher.py)
- Python 3.12
Description
JSONRPCDispatcher.send_raw_requestattaches_metatoparamsunconditionally:When no progress callback is set and the otel tracer is a no-op (default),
out_metastays{}and every request ships_meta: {}.Strict JSON-RPC servers reject this: Meta's hosted Ads MCP server (
https://mcp.facebook.com/ads) returns HTTP 400 with-32602 "_meta for Request must be a dict or null"oninitialize, making it unreachable from any client built on this SDK._meta: nullis rejected identically — the field must be absent.Other MCP clients Meta documents (Claude Code, ChatGPT) don't emit an empty
_metaand connect fine.Reproduction
Reported downstream from Hermes Agent (NousResearch/hermes-agent#105637), with a minimal curl repro against the real server (400 with
_meta:{}}, 200 without). Happy to attach a self-contained repro against a local strict server if preferred.Proposed fix
after
inject_trace_context(out_meta)so a real trace context is still sent.Environment
src/mcp/shared/jsonrpc_dispatcher.py)