Skip to content

[Bug] Client sends empty _meta:{} on every request; strict servers (Meta Ads MCP) reject with HTTP 400 #3473

Description

@kokhlo

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

Activity

  1. jstar0 commented on Sep 8, 2026

    @jstar0

    I verified this against the current main: JSONRPCDispatcher.send_raw_request still creates and emits _meta: {} when the caller provides no metadata, no progress callback is registered, and trace propagation has no values to inject. The focused fix should omit _meta only when it remains empty after trace-context injection, while preserving non-empty caller metadata and an injected progressToken. The caller-provided params should remain untouched. This is a protocol-compatibility bug rather than a security report; I am not proposing a broader refactor. Since you reported the issue, you have first call if the maintainers accept an outside implementation. If maintainers welcome an external PR after triage, I can prepare a small regression-tested patch within this scope. AI-assisted contribution: I reviewed the current source and repository contribution rules and will personally verify any patch and answer review questions.

  2. kokhlo commented on Sep 8, 2026

    @kokhlo
    Author

    Maintainers: could you assign this issue to me? I have a fix ready (PR #3474 was auto-closed by the missing-issue-link policy — happy to reopen it once the issue is assigned to my handle).

  3. added
    v2Affects the v2 line (2.x on main)
    on Sep 10, 2026
  4. maxisbey commented on Oct 2, 2026

    @maxisbey
    Contributor

    This is fixed in #3628 with the change you proposed: _meta is now only sent when there's something in it. Thanks for the clear report and for having a fix ready in #3474.

    A request left with no params at all (ping, tools/list) also goes out without a params member, which is the shape v1 sent.

    It hasn't been tried against the real Meta Ads endpoint, so if that still refuses the connection, please open a new issue.

    AI Disclaimer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    v2Affects the v2 line (2.x on main)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions