rohitg00/ai-engineering-from-scratch · error · ValueError

_meta must be an object

Error message

_meta must be an object

What it means

The MCP simulator's _validate_request_metadata requires every request params object to carry a `_meta` key that is a dict (JSON object). If params has no `_meta` at all, or `_meta` is a string/number/list/None, the exchange rejects the request before dispatch. This mirrors the spec's rule that stateless MCP requests embed protocol negotiation metadata in `_meta`.

Source

Thrown at certifications/claude/lessons/11-mcp-server-design-and-integration/code/main.py:207

            raise ValueError("unsupported or malformed notification")
        request_id = params.get("requestId")
        if type(request_id) not in (str, int):
            raise ValueError("notifications/cancelled requires a requestId")
        reason = params.get("reason")
        if reason is not None and not isinstance(reason, str):
            raise ValueError("notifications/cancelled reason must be a string")
        metadata = params.get("_meta")
        if metadata is not None and not isinstance(metadata, dict):
            raise ValueError("notifications/cancelled _meta must be an object")
        if set(params) - {"requestId", "reason", "_meta"}:
            raise ValueError("notifications/cancelled contains unexpected fields")

    def _validate_request_metadata(self, params: Any) -> dict[str, Any]:
        if not isinstance(params, dict):
            raise ValueError("params must be an object")
        metadata = params.get("_meta")
        if not isinstance(metadata, dict):
            raise ValueError("_meta must be an object")
        version = metadata.get(PROTOCOL_VERSION_KEY)
        capabilities = metadata.get(CLIENT_CAPABILITIES_KEY)
        if not isinstance(version, str):
            raise ValueError(f"_meta.{PROTOCOL_VERSION_KEY} is required")
        if not isinstance(capabilities, dict):
            raise ValueError(f"_meta.{CLIENT_CAPABILITIES_KEY} is required")
        client_info = metadata.get(CLIENT_INFO_KEY)
        if client_info is not None and (
            not isinstance(client_info, dict)
            or not isinstance(client_info.get("name"), str)
            or not isinstance(client_info.get("version"), str)
        ):
            raise ValueError(f"_meta.{CLIENT_INFO_KEY} must include name and version")
        if version != CURRENT_PROTOCOL_VERSION:
            raise ProtocolError(
                -32022,
                "Unsupported protocol version",
                {"supported": [CURRENT_PROTOCOL_VERSION], "requested": version},

View on GitHub (pinned to 39ea8a1c6d)

Solutions

  1. Add a _meta object to every request params: {"_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {}}}
  2. If _meta is present but a string, json.loads it or wrap it as an object before sending
  3. Centralize _meta construction in one client helper so no call site can forget it

Example fix

# before
params = {"uri": "study://catalog"}
server.exchange(method, params)
# after
params = {
  "uri": "study://catalog",
  "_meta": {
    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
    "io.modelcontextprotocol/clientCapabilities": {},
  },
}
server.exchange(method, params)
Defensive patterns

Strategy: validation

Validate before calling

def build_params(payload: dict) -> dict:
    payload["_meta"] = payload.get("_meta") or {}
    assert isinstance(payload["_meta"], dict), "_meta must be an object"
    return payload

Type guard

def has_valid_meta(params: dict) -> bool:
    return isinstance(params.get("_meta"), dict)

Try / catch

try:
    server.exchange(method, params)
except ValueError as e:
    if "_meta" in str(e):
        params = {**params, "_meta": default_meta()}
        server.exchange(method, params)

Prevention

When it happens

Trigger: Calling exchange() (or server/discover, resources/read, prompts/get, tools/call through it) with params like {}, {"uri": ...} with no _meta, or {"_meta": "2026-07-28"} (a string instead of an object).

Common situations: Client code built for a stateful MCP transport (initialize handshake once, then plain params) ported to this stateless design; hand-written JSON payloads where _meta is omitted or accidentally double-encoded as a JSON string; test fixtures missing the _meta block.

Understand the failure class

Background: "Missing required field" and "field is required" errors: why libraries reject payloads that omit mandatory fields — this error's family across 20 libraries.

Related errors


AI-assisted analysis of rohitg00/ai-engineering-from-scratch@39ea8a1c6d (2026-08-26). Data as JSON: /api/errors/1a34913839ea8a4c. Report an issue: GitHub.