Skip to content

MCP client sends empty _meta:{} on every request → Meta Ads MCP server rejects with HTTP 400 #105637

Description

@GediminasGrazys

Bug: MCP client sends empty _meta:{} on every request → Meta Ads MCP server rejects with HTTP 400

Summary

Hermes cannot natively connect to Meta's official hosted Ads MCP server
(https://mcp.facebook.com/ads). Every JSON-RPC request the bundled MCP client sends
carries an empty _meta: {} object in params. Meta's server strictly rejects this
with HTTP 400 (-32602 "_meta for Request must be a dict or null"), so initialize
fails and the server is parked after 3 attempts.

Other MCP clients Meta officially documents (Claude Code, ChatGPT) do not emit an
empty _meta, so they connect fine — this is a client-side compatibility issue, not a
Meta outage.

Root cause

mcp/shared/jsonrpc_dispatcher.py (vendored MCP SDK), send_raw_request, ~lines 356–362:

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                       # unconditionally attaches _meta, even when empty

out_params["_meta"] is set even when out_meta is {} (no progress token, no trace
context from the no-op tracer). The wire payload becomes:

{"jsonrpc":"2.0","id":1,"method":"initialize",
 "params":{"protocolVersion":"2025-11-25","capabilities":{},
           "clientInfo":{"name":"mcp","version":"0.1.0"},"_meta":{}}}

Reproduction (identical requests, only _meta differs)

# WITH _meta:{}  -> HTTP 400
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://mcp.facebook.com/ads \
  -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  --data-raw '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"mcp","version":"0.1.0"},"_meta":{}}}'

# WITHOUT _meta   -> HTTP 200
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://mcp.facebook.com/ads \
  -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  --data-raw '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"mcp","version":"0.1.0"}}}'

Confirmed on this machine: 400 vs 200 respectively. _meta:null is ALSO rejected 400 by
Meta, so passing null is not a workaround — the field must be omitted entirely.

Suggested fix

Only attach _meta when it has content:

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
# ... trace-context injection into out_meta ...
if out_meta:                        # <-- only set when non-empty
    out_params["_meta"] = out_meta
elif "_meta" in out_params:
    del out_params["_meta"]

Note inject_trace_context(out_meta) (line ~390) may add W3C trace keys; the emptiness
check must run AFTER that injection so a real trace context is still sent. When the tracer
is a no-op (default), out_meta stays {} and should be omitted.

Impact

Any strict MCP server that rejects an empty _meta object is unreachable natively from
Hermes. Meta's hosted Ads MCP server is the first confirmed case. Current workaround:
call the server over raw curl (which omits _meta) — works, but bypasses native MCP
integration and the settings UI.

Environment

  • Hermes MCP client: bundled mcp SDK under hermes-agent/venv/.../mcp/
  • Server: Meta Ads MCP Server v1.0.0, https://mcp.facebook.com/ads
  • Protocol negotiated: server replies 2025-06-18

Activity

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

    P2Medium — degraded but workaround existscomp/toolsTool registry, model_tools, toolsetstool/mcpMCP client and OAuthtype/bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions