Skip to content

JSON-RPC Messages and _meta

Every MCP message is a JSON-RPC 2.0 message. There are only four shapes, and once you can read them you can read any MCP traffic.

flowchart LR
  R["Request (id, method, params)"] --> OK["Result response (id, result)"]
  R --> ERR["Error response (id, error)"]
  N["Notification (method, params) - no id"]
Requests get a response; notifications do not
{ "jsonrpc": "2.0", "id": 7, "method": "tools/call", "params": { "name": "get_weather", "arguments": { "location": "Seattle" } } }
  • id must be a string or integer, never null.
  • id must not be reused while a request with the same id is still in flight.
  • In the current revision, requests flow from client to server. Servers do not send their own requests any more; when they need something from the client they say so in the result (see resultType below).
{ "jsonrpc": "2.0", "id": 7, "result": { "resultType": "complete", "content": [ { "type": "text", "text": "Sunny" } ] } }
  • Same id as the request.
  • result is any object, but it must include resultType.
{ "jsonrpc": "2.0", "id": 7, "error": { "code": -32602, "message": "Unknown tool: get_wether" } }
  • Same id, unless the request was so malformed the id could not be read.
  • code is an integer; message is a string; data is optional and free-form.
{ "jsonrpc": "2.0", "method": "notifications/progress", "params": { "progressToken": "abc", "progress": 3, "total": 10 } }
  • No id, and the receiver must not reply.

The resultType field tells the client what kind of result it is holding:

ValueMeaning
"complete"The request finished; this is the final content.
"input_required"The server needs more from the client before it can finish (an elicitation, for example). The result is an InputRequiredResult, and the client retries the original request with the answers.
extension-definedExtensions may add values (the Tasks extension adds "task"), but only ones both sides negotiated via capabilities.

Two rules keep this safe. An unrecognised resultType must be treated as invalid. And a result that omits the field — which happens with servers on legacy revisions (2025-11-25 and earlier) — must be treated as "complete".

MCP reuses the standard JSON-RPC codes and carves up the implementation-defined range:

CodeNameWhen
-32700Parse errorThe body was not valid JSON
-32600Invalid requestNot a valid JSON-RPC request object
-32601Method not foundUnknown method (on HTTP, paired with status 404)
-32602Invalid paramsBad or missing params — also unknown tool, unknown prompt, resource not found, and a request missing required _meta fields
-32603Internal errorSomething broke inside the server
-32020HeaderMismatchStreamable HTTP: a mirrored header disagrees with the body, or a required header is missing
-32021MissingRequiredClientCapabilityThe request needs a capability the client did not declare; data.requiredCapabilities lists them
-32022UnsupportedProtocolVersionThe server does not speak the requested version; data.supported lists what it does

The range -32000 to -32019 is legacy: SDKs used it before the policy existed, and new code must not allocate there. One code from it still matters: -32002 was “resource not found” in 2025-11-25 and earlier. Current servers must send -32602 instead, but clients should still accept -32002 from older servers. -32020 to -32099 is reserved for the specification, so never emit a code there that the spec has not defined. If you need application errors, use codes outside -32768..-32000 entirely.

There is one more category that is not a JSON-RPC error at all: a tool execution error. A tool that fails in a way the model could fix (bad date format, record not found) returns a normal result with isError: true and an explanatory message. That distinction is covered in Server Primitives → Tools.

_meta is a bag of key/value pairs that can appear on request params and on result objects. MCP uses it for protocol-level metadata so the main payload stays clean.

A key is an optional prefix (dot-separated labels ending in /, reverse-DNS style: com.example/) followed by a name (alphanumeric at both ends, hyphens/underscores/dots inside). Any prefix whose second label is modelcontextprotocol or mcp is reserved for the protocol — io.modelcontextprotocol/ is the one you will see everywhere. com.example.mcp/ is fine, because its second label is example.

KeyPurpose
progressTokenOpts a request into progress notifications
io.modelcontextprotocol/protocolVersionThe version this request speaks
io.modelcontextprotocol/clientInfoClient name and version
io.modelcontextprotocol/clientCapabilitiesClient capabilities relevant to this request
io.modelcontextprotocol/logLevelMinimum log level the server should emit for this request
io.modelcontextprotocol/subscriptionIdTies a notification to the subscriptions/listen that opened it
io.modelcontextprotocol/serverInfoServer name and version, on results
traceparent, tracestate, baggageOpenTelemetry trace context — the one exception to the prefix rule, kept for compatibility with existing tracing conventions

Two keys are required on every client request; a request missing either is malformed and the server rejects it with -32602 (HTTP status 400):

KeyRequired
io.modelcontextprotocol/protocolVersionYes
io.modelcontextprotocol/clientCapabilitiesYes
io.modelcontextprotocol/clientInfoNo, but clients should send it
io.modelcontextprotocol/logLevelNo

Because capabilities ride on every request, a server never has to remember what the client declared earlier. If a request needs a capability the client did not include, the server returns -32021 rather than guessing.

Servers, for their part, should put io.modelcontextprotocol/serverInfo in every result’s _meta. Both clientInfo and serverInfo are self-reported and unverified: use them for logs and UI, never for security decisions.

{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": { "location": "New York" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"traceparent": "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01"
}
}
}

Attach a W3C traceparent and your tool calls show up as spans in the same trace as the host’s request. It costs one line and pays for itself the first time you debug a slow tool.

Tool input and output schemas, elicitation schemas, and structured content all use JSON Schema. Three rules:

  1. Default dialect is 2020-12. A schema without $schema is read as JSON Schema 2020-12. Implementations must support it and may support others if the schema declares them.
  2. No network $ref by default. A $ref pointing at an absolute URI must not be dereferenced automatically; an implementation may offer an opt-in fetch mode, but it must be off by default and should block private and loopback addresses. A schema that cannot validate because of an unresolved external $ref should be rejected, not silently accepted.
  3. Bound composition. anyOf/oneOf/allOf/if-then-else and $defs are allowed, but implementations should cap depth and subschema count so a hostile schema cannot become a denial-of-service against the validator.

Everything above is what crosses the wire. In practice the SDK builds it. Here is a tool call from a client in each language; note that neither touches _meta, id, or resultType:

import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
const client = new Client({ name: 'my-client', version: '1.0.0' });
await client.connect(new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp')));
// The SDK adds protocolVersion, clientInfo, clientCapabilities and the id.
const result = await client.callTool({ name: 'get_weather', arguments: { location: 'Seattle' } });
if (result.isError) console.error('tool error:', result.content);
else console.log(result.content);

Knowing the wire format still matters: it is what you read in the Inspector’s traffic pane, what proxies validate, and what you fall back to when an SDK abstraction leaks.

Which of these is a valid JSON-RPC request id in MCP?
A result from a legacy server has no resultType. How must a client treat it?
Which _meta keys are required on every client request?
A current server cannot find a requested resource. Which error code does it send?