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
- Add a _meta object to every request params: {"_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {}}}
- If _meta is present but a string, json.loads it or wrap it as an object before sending
- 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
- Build every request through one helper that always injects a _meta object
- Never hand-write params dicts at call sites; assemble them from a typed client class
- Add a unit test asserting all outbound payloads contain a dict _meta
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
- -32600
- -32601
- _meta.{PROTOCOL_VERSION_KEY} is required
- _meta.{CLIENT_CAPABILITIES_KEY} is required
- _meta.{CLIENT_INFO_KEY} must include name and version
AI-assisted analysis of rohitg00/ai-engineering-from-scratch@39ea8a1c6d (2026-08-26).
Data as JSON: /api/errors/1a34913839ea8a4c.
Report an issue: GitHub.