# JSON-RPC Messages

Course: [Model Context Protocol](https://www.devtutorials.ai/learn/mcp) · Section: Protocol Architecture
Source: https://www.devtutorials.ai/learn/mcp/architecture/json-rpc-messages

> Every field of MCP requests, responses and notifications, id matching, the core methods, _meta and resultType, error codes, tool errors versus protocol errors, and why servers are now stateless.

When you fill in a form at a bank, each box has a set place: your name here, your account number there, and your signature at the bottom. The clerk knows where to find each detail without searching through the whole form.

MCP messages work the same way. Each field has a specific place and purpose. Once you know those fields, you can read a message and understand what it asks or what answer it carries.

**The main idea:** an MCP message is a JSON object with a defined structure. A request carries a `method`, an `id`, and `params`. A response carries the same `id` and either a `result` or an `error`. A notification carries a `method` but no `id`.

To see why these fields matter, let's follow a developer as they diagnose a failed request.

## A Real-World Example

Brightgrid's assistant is slow on Monday mornings. Ravi, a developer, opens the Inspector and sees this:

- Request id 4: `tools/call` for `lookup_order`.
- Response for id 4: an error with code `-32602`.

Because the response has the same id, Ravi can immediately see that the error belongs to his request. The code means "invalid params." Ravi checks the error message and sees that the model sent `order_id` as a number instead of text. He improves the tool description so the model is less likely to make the same mistake.

**In simple words:** message fields and error codes help Ravi move from "something broke" to a specific problem he can investigate.

## What You Will Learn

In this lesson, you will learn:

1. The fields of requests, responses and notifications.
2. How ids match answers to questions.
3. The core methods and what they return.
4. What `_meta` and `resultType` are for.
5. The error codes, and how a tool error differs from a protocol error.
6. Why MCP servers are stateless in the 2026-07-28 spec, and why sessions and `ping` were removed from that version.

The **MCP Lab** has a **Raw request** tab. It lets you write an HTTP request by hand and send it straight to the server, with no client in between. It includes ready-made examples, including invalid requests.

## The Three Frames

Before looking at real messages in the Lab, it helps to understand the three basic message shapes used throughout MCP.

### Request Frame

A **request** asks the server to do something. It has four main fields:

| Field | Meaning |
| --- | --- |
| `jsonrpc` | The JSON-RPC version. For MCP messages, it is `"2.0"` |
| `id` | A number or text the client chooses to match this request with its response. Each open request needs a different id |
| `method` | The operation to perform, such as `"tools/call"` |
| `params` | The information the method needs, plus request metadata in `_meta` |

Here is a small request builder. It gives each request a different id and adds the metadata required by this course's current protocol version.

```js runnable title="A request builder"
// Build any MCP request. Every request in 2026-07-28 carries the same _meta.
let nextId = 1;
function makeRequest(method, params = {}) {
  return {
    jsonrpc: "2.0",
    id: nextId++,
    method,
    params: {
      ...params,
      _meta: {
        "io.modelcontextprotocol/protocolVersion": "2026-07-28",
        "io.modelcontextprotocol/clientInfo": { name: "my-host", version: "1.0.0" },
        "io.modelcontextprotocol/clientCapabilities": {},
      },
    },
  };
}

const a = makeRequest("tools/list");
const b = makeRequest("tools/call", {
  name: "lookup_order",
  arguments: { order_id: "A-1001" },
});
console.log(a.id, a.method, Object.keys(a.params).join(","));
console.log(b.id, b.method, Object.keys(b.params).join(","));
```

Expected output:

```text
1 tools/list _meta
2 tools/call name,arguments,_meta
```

### Response Frame

A **response** answers one request. It has `jsonrpc`, the same `id` as the request, and exactly one of these:

- `result`: an object with the answer. In this spec version, it includes a `resultType`.
- `error`: an object with a numeric `code`, a `message`, and sometimes `data` with more detail.

### Notification Frame

A **notification** sends information without expecting a response. It has `jsonrpc` and a `method`, and may have `params`, but it has **no `id`**.

Let's look at a real request in the Lab and identify its fields in the server's reply.

Press **Send**, then read the reply. Find the `id`, the `result`, the `content`, and the `resultType`.

The response repeats the request's `id`. That shared id is how a client matches each answer to the request it belongs to, even if answers arrive out of order.

## Id Matching

### Why Ids Matter

A client can send several requests before any answer comes back. Answers may arrive in a different order. The `id` tells the client which request each response answers.

Rules for ids:

- The client chooses them. They can be numbers or text.
- An id must not be reused while its request is still waiting for a response.
- A response with an id the client is not waiting for can be ignored.

```js runnable title="Match answers to questions"
// Answers can come back in any order. The client matches them by id.
const waiting = new Map([
  [1, "tools/list"],
  [2, "resources/read kb://shipping"],
  [3, "tools/call export_report"],
]);

const arrived = [
  {
    jsonrpc: "2.0",
    id: 2,
    result: {
      contents: [{ uri: "kb://shipping", text: "3 to 6 days" }],
      resultType: "complete",
    },
  },
  { jsonrpc: "2.0", id: 1, result: { tools: [], resultType: "complete" } },
  { jsonrpc: "2.0", id: 9, result: { resultType: "complete" } },
  {
    jsonrpc: "2.0",
    id: 3,
    result: { content: [{ type: "text", text: "Report ready" }], resultType: "complete" },
  },
];

for (const r of arrived) {
  if (!waiting.has(r.id)) {
    console.log(`id ${r.id}: nobody asked for this, ignore it`);
    continue;
  }
  console.log(`id ${r.id}: answer to ${waiting.get(r.id)}`);
  waiting.delete(r.id);
}
console.log("still waiting:", waiting.size);
```

Expected output:

```text
id 2: answer to resources/read kb://shipping
id 1: answer to tools/list
id 9: nobody asked for this, ignore it
id 3: answer to tools/call export_report
still waiting: 0
```

Now that you've seen how ids connect requests and responses, let's look at the methods that tell the server what operation to perform.

## Core Methods

These are the methods you will use most. Each method names an operation, such as listing tools or reading a resource. The name usually combines an area (`tools`, `resources`, or `prompts`) with an action (`list`, `call`, `read`, or `get`).

```js runnable title="The core methods"
// The core methods and what each one returns.
const methods = [
  ["server/discover", "versions, capabilities, instructions"],
  ["tools/list", "tools: name, description, inputSchema"],
  ["tools/call", "content blocks (and isError on failure)"],
  ["resources/list", "resources: uri, name, mimeType"],
  ["resources/read", "contents: uri + text or blob"],
  ["prompts/list", "prompts: name, description, arguments"],
  ["prompts/get", "messages: role + content"],
  ["completion/complete", "completion: values, total, hasMore"],
];

for (const [m, returns] of methods) {
  const [area, verb] = m.split("/");
  console.log(`${area.padEnd(10)} ${verb.padEnd(9)} -> ${returns}`);
}
```

Expected output:

```text
server     discover  -> versions, capabilities, instructions
tools      list      -> tools: name, description, inputSchema
tools      call      -> content blocks (and isError on failure)
resources  list      -> resources: uri, name, mimeType
resources  read      -> contents: uri + text or blob
prompts    list      -> prompts: name, description, arguments
prompts    get       -> messages: role + content
completion complete  -> completion: values, total, hasMore
```

`completion/complete` is a helper that suggests values while the user types an argument, like autocomplete in a search box. It works for prompt arguments and resource templates.

The server suggests `A-1001` and `A-1002` because both start with `A-10`.

The method tells the server which operation to perform. Requests also carry metadata about the protocol and the client. That information lives in `_meta`.

## The _meta Field

### What It Carries

`_meta` is a special field inside request `params` or a result. It holds metadata about the request or result, not the method or answer itself. In 2026-07-28, every request's `_meta` carries:

- `io.modelcontextprotocol/protocolVersion`: the spec version, such as `"2026-07-28"`.
- `io.modelcontextprotocol/clientInfo`: the client's name and version.
- `io.modelcontextprotocol/clientCapabilities`: features the client supports, such as showing a form to the user.

It may also carry a `progressToken`. Results carry `io.modelcontextprotocol/serverInfo` in their `_meta`.

The `io.modelcontextprotocol/` prefix identifies these as MCP metadata keys and helps avoid name collisions.

### resultType

A result's `resultType` tells the client what kind of result it received. Two common values are:

- `"complete"`: the server has finished and returned an answer.
- `"input_required"`: the server needs more input. The client collects the answers and sends the request again (MRTR).

A client should check `resultType` before it reads the rest of the result. Some extensions may define other result types.

List results, like `tools/list`, also carry `ttlMs` and `cacheScope`. They tell the client how long it may keep the list before asking again. You will use them in the caching lesson in Chapter 7.

The message fields tell the client how to read a response. The next question is what to do when that response reports a problem. JSON-RPC uses error codes to describe protocol-level failures.

## Error Codes

### The Standard Codes

JSON-RPC defines several standard error codes. MCP also defines codes for cases such as HTTP header mismatches and unsupported protocol versions:

```js runnable title="Explain an error code"
// Error codes you will meet in MCP 2026-07-28.
const codes = {
  "-32700": "Parse error: the body is not valid JSON",
  "-32600": "Invalid request: not a proper JSON-RPC request",
  "-32601": "Method not found",
  "-32602": "Invalid params (also: resource not found, unknown tool)",
  "-32603": "Internal error inside the server",
  "-32020": "HTTP headers and body disagree",
  "-32022": "Unsupported protocol version",
};

function explain(errorReply) {
  const { code, message } = errorReply.error;
  return `${code}: ${codes[code] ?? "a server-specific error"} | server said "${message}"`;
}

console.log(
  explain({
    jsonrpc: "2.0",
    id: 3,
    error: { code: -32601, message: "Method not found" },
  }),
);
console.log(
  explain({
    jsonrpc: "2.0",
    id: 5,
    error: { code: -32022, message: "Unsupported protocol version: 2099-01-01" },
  }),
);
console.log(
  explain({
    jsonrpc: "2.0",
    id: 7,
    error: { code: -32001, message: "Database is busy" },
  }),
);
```

Expected output:

```text
-32601: Method not found | server said "Method not found"
-32022: Unsupported protocol version | server said "Unsupported protocol version: 2099-01-01"
-32001: a server-specific error | server said "Database is busy"
```

Try a few invalid requests in the Lab. Each one shows a different kind of protocol error:

### Tool Errors vs Protocol Errors

These two kinds of failure are returned differently:

- A **protocol error** means the request itself could not be processed, such as when it contains broken JSON or an unknown method. The server sends a JSON-RPC `error`.
- A **tool error** means the request was understood, but the requested work failed: perhaps the order doesn't exist, the database is down, or the arguments didn't match the tool's schema. The server sends a normal `result` with `isError: true` and a message in `content`.

A tool error goes back to the **model**, which can read the message and may try again. A protocol error usually means the client or host needs to fix the request.

In 2026-07-28, "resource not found" uses code `-32602`. Older servers used `-32002`. If you write a client, accept both for a while, because many servers are not updated yet.

Error codes help explain a single request and response. A related design change affects how servers handle every request: the current protocol version is stateless.

## Stateless Processing

### What Stateless Means

A **stateless** server does not rely on information saved in a session from an earlier request. Each request carries its own protocol version, client information, capabilities, and the parameters the method needs. That is why `_meta` appears on every request.

This makes it easier to run several copies of a server behind a load balancer. Any copy can handle a request without first looking up session information stored by another copy.

```js runnable title="Every request must stand on its own"
// A stateless server: every request must carry everything the server needs.
function check(req) {
  const meta = req.params?._meta ?? {};
  const missing = ["protocolVersion", "clientInfo", "clientCapabilities"].filter(
    (k) => !(`io.modelcontextprotocol/${k}` in meta),
  );
  return missing.length
    ? "rejected, missing " + missing.join(", ")
    : "ok, can be handled by any server copy";
}

const full = {
  method: "tools/list",
  params: {
    _meta: {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientInfo": { name: "host", version: "1" },
      "io.modelcontextprotocol/clientCapabilities": {},
    },
  },
};
const bare = { method: "tools/list", params: {} };
const oldStyle = {
  method: "tools/list",
  params: {},
  headers: { "Mcp-Session-Id": "abc123" },
};

console.log("full request:       ", check(full));
console.log("bare request:       ", check(bare));
console.log("old session request:", check(oldStyle));
```

Expected output:

```text
full request:        ok, can be handled by any server copy
bare request:        rejected, missing protocolVersion, clientInfo, clientCapabilities
old session request: rejected, missing protocolVersion, clientInfo, clientCapabilities
```

### Why Sessions Were Removed

Older protocol versions used **sessions**. The first request, `initialize`, created a session, and the server sent back an `Mcp-Session-Id`. Later requests had to include that id and could depend on reaching the same server copy. This made scaling and recovery harder. The 2026-07-28 version removed sessions. The protocol version and client details now travel with each request in `_meta`.

### Why ping Was Removed

Older versions used `ping` to check whether the other side was still reachable over a long-lived connection. The 2026-07-28 version has no sessions or `ping` method. A client that wants to check whether a server is responding can send `server/discover`.

The server answers with `-32601`, which means the method was not found. In this protocol version, `ping` is not a method.

## Common mistakes

- **Forgetting `jsonrpc: "2.0"`.** Without it, the message is not valid JSON-RPC.
- **Mixing up `error` and `isError`.** `error` is a protocol error. `isError: true` is inside a normal result and means the tool failed.
- **Sending `_meta` only on the first request.** Servers are stateless. Every request needs it.
- **Matching answers by order.** Always match by `id`. Answers can arrive in any order.
- **Calling `ping` or `initialize` on a server using the 2026-07-28 protocol.** Those methods are not part of that version. Use `server/discover` to check whether the server responds.

The ideas in this lesson fit together: message shape tells you what kind of message you received, ids connect requests to responses, and errors and metadata help explain what happened. Use the quiz to check your understanding.

## Exercises

Each exercise ends with a value on the last line. Press **Check** to compare it with the expected answer.

## What to Remember

- **Request:** `jsonrpc`, `id`, `method`, `params`. **Response:** same `id`, plus `result` or `error`. **Notification:** `method`, no `id`.
- Match answers by **id**, never by order.
- Core methods: `server/discover`, `tools/list` and `tools/call`, `resources/list` and `resources/read`, `prompts/list` and `prompts/get`, and `completion/complete`.
- Every request's **`_meta`** carries the version, client info and client capabilities. Every result has a **`resultType`**.
- **Protocol errors** use JSON-RPC `error` codes like -32601, -32602, -32020 and -32022. **Tool errors** are results with `isError: true`.
- Servers are **stateless**: no sessions, no `ping`.

Next: **Transports**, how messages travel through stdio and Streamable HTTP.
