Editorial

Research guide · MCP interoperability · 13 September 2026

MCP _meta HTTP 400: fix empty request metadata without breaking 2026-07-28

If a hosted MCP server works from one client but another fails immediately with HTTP 400, inspect the raw request before changing authentication or tool code. A current MCP Python SDK issue reports a particularly sharp interoperability failure: the client serializes _meta: {} on every request, while Meta's hosted Ads MCP server rejects that empty field during initialize. The fix depends on which MCP protocol era is actually on the wire.

Published 13 September 2026 · sources checked 13 September 2026

#3473

Open Python SDK interoperability report

HTTP 400

Reported failure against Meta Ads MCP

≤ 2025-11-25

Handshake-era MCP lifecycle

2026-07-28

Stateless era with per-request metadata

The exact symptom

An empty object is not always equivalent to an absent field

Python SDK issue 3473, opened 8 September 2026, says JSONRPCDispatcher.send_raw_request currently builds an _meta object and attaches it to params even when no progress callback, tracing context or other metadata populated it. The result is a wire payload containing an empty object.

The reporter tested Meta's hosted Ads MCP endpoint and observed HTTP 400 with JSON-RPC error -32602 on initialize when the request contained either an empty _meta object or null. In the reported reproduction, leaving the field out allowed the legacy initialization request to proceed. That is implementation evidence from an open issue, not a universal rule that all MCP metadata must be removed.

That qualification matters because the current MCP protocol has two lifecycle eras. A workaround that is correct for a strict handshake-era server can break a native 2026-07-28 request.

Protocol-era test

First decide whether you should omit metadata or populate it

Wire shapeInterpretationSafe first move
initialize present; _meta is emptyHandshake-era request carrying meaningless metadataOmit _meta when it has no progress, trace or extension content; preserve any real metadata.
initialize present; _meta omittedNormal legacy shape when no request metadata is neededKeep it absent unless the negotiated feature actually needs metadata.
2026-07-28 request; _meta is emptyModern request missing required per-request protocol contextDo not merely strip it. Populate the reserved protocol-version and client-capability keys through the SDK.
2026-07-28 request; complete _metaModern stateless lifecycleValidate the reserved values on every request and keep client identity/version handling current.

Why 2026-07-28 changes the answer

Modern MCP intentionally moves connection context onto every request

MCP 2026-07-28 retired the initialize/initialized handshake and protocol-level sessions. In the modern lifecycle, each request carries its own protocol context. Official MCP documentation says protocol version and client capabilities now travel in _meta on every request; current SDK documentation treats client identity as recommended rather than mandatory.

That means a modern request with _meta: {} has a different problem from the Meta Ads report: it is not merely carrying a redundant field, it is missing the metadata that replaced the handshake. The correct repair is to make the session layer serialize the required reserved entries, not to delete the envelope globally.

Conversely, versions through 2025-11-25 still use initialize. On that path, do not inject an empty metadata object just because the SDK has a common request model. Preserve metadata when a real feature needs it; otherwise omission can be the interoperable representation for strict peers.

Wire-level diagnosis

Six checks before you patch a vendor or fork an SDK

  1. 1. Capture the failing JSON-RPC request. Look at the actual serialized params, not the in-memory model. Confirm whether _meta is absent, empty, null or populated.
  2. 2. Identify the lifecycle from the wire. If the client is sending initialize, you are on the handshake path. A native 2026-07-28 request should not be using that initialization exchange.
  3. 3. Compare against a working client. Keep endpoint, credentials and requested operation constant. A different serialization shape with the same server is stronger evidence than changing several variables at once.
  4. 4. Preserve meaningful metadata. Progress tokens, trace context, extensions and modern reserved keys must not be lost because an empty-object bug exists elsewhere.
  5. 5. Test the smallest era-correct repair. On a handshake request with no metadata semantics, omit an empty field. On 2026-07-28, make the client populate the required protocol metadata.
  6. 6. Remove compatibility patches when upstream behavior changes. Vendor-specific serialization workarounds should be version-gated and temporary. Re-test after SDK releases rather than turning a transient interop defect into your permanent protocol contract.

Minimal mental model

Empty, absent and populated are three different states

Absent

No request metadata is being asserted. This can be correct on a handshake-era request when no metadata-bearing feature is in use.

Empty

The field exists but conveys nothing. A strict implementation may reject this shape, and it is insufficient for a native 2026-07-28 request.

Populated

The field carries real progress, trace, extension or protocol context. Validate its keys for the negotiated protocol era instead of deleting it.

Related zFinia paths

Keep metadata failures separate from routing, OAuth and server trust

Sources

Primary implementation and protocol references

Bottom line

If a strict MCP server rejects _meta: {}, do not jump from “empty metadata is bad here” to “metadata should never be sent.” First identify the protocol era. On handshake-era traffic, omit metadata that has no semantic content. On MCP 2026-07-28, send the meaningful per-request protocol context the stateless lifecycle requires. That distinction fixes the interoperability bug without creating the next one.