# Protocol Architecture

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

> The three layers of MCP (application, message, transport), the three kinds of messages, progress updates, cancellation on each transport, and how server requests became Multi Round-Trip Requests.

Think about sending a parcel. First, you decide what to send, such as a birthday gift. Next, you pack it in a box and add a label. Finally, a truck carries the box to its destination.

The gift doesn't need to know how it is transported, and the truck doesn't need to know what is inside the box.

MCP works in a similar way, with different layers handling different jobs. Each layer has one job and doesn't need to know how the other layers do theirs. That is why the same MCP message can travel through a local pipe on your laptop or across the internet.

The main idea: MCP has three layers. The application layer says what to do. The message layer packs it as JSON-RPC. The transport layer carries the bytes.

To see how these three layers work together, let's look at a real-world example.

## A Real-World Example

Maya asks Brightgrid's assistant for a sales report. The report takes a few seconds to build.

- The host asks to use the `export_report` tool to build the report. That is the application layer.
- The client packs the call as a JSON-RPC `tools/call` request with an id. That is the message layer.
- The request travels over HTTP to the help-desk server. That is the transport layer.
- While the server works, it sends **progress** updates, such as "part 1 of 5" and "part 2 of 5". These updates show how much work is done; you'll learn more about them later in this lesson.
- If Maya decides to stop waiting, she can press **Stop**. The client sends a cancellation signal, and the server tries to stop the work.

**In simple words:** layers keep the jobs apart. Messages ask, answer, or give a notice. Long-running work can report progress and can be cancelled.

## What You Will Learn

In this lesson, you will learn:

1. The three layers of MCP.
2. The three kinds of messages: requests, responses and notifications.
3. Who may send which message in the current spec.
4. How progress updates work.
5. How cancellation works.
6. How servers ask the user for input today, with Multi Round-Trip Requests.

The **MCP Lab** at the bottom of the page has a slow tool, `export_report`, that sends progress messages. It also has a **Stop** button, so you can try cancelling a request.

## The Three Layers

### Application Layer

The **application layer** is about meaning. It covers the things you met in Chapter 1: tools, resources and prompts, plus the rules for discovery and for asking the user. When you write `server.registerTool(...)` in an SDK, you work in this layer.

### Message Layer

The **message layer** turns each action into a **JSON-RPC 2.0** message. JSON-RPC is a small standard that defines the shape of a request and its reply. A request has a `method` and an `id`, and it may also have `params`. A response has the same `id` and either a `result` or an `error`.

This layer doesn't need to understand what a method does. It makes sure messages have the right shape and that each response can be matched to its request.

### Transport Layer

The **transport layer** moves the messages. MCP defines two standard transports:

- **stdio:** the host starts the server as a program on the same computer. Messages go through the program's input and output, one JSON message per line.
- **Streamable HTTP:** the server runs as a web service. Each request is an HTTP `POST`.

You will study both in the Transports lesson.

```js runnable title="One call, three layers"
// One tool call, wrapped by each layer in turn.

// Application layer: what the host wants.
const intent = { tool: "lookup_order", args: { order_id: "A-1001" } };

// Message layer: the same thing as a JSON-RPC request.
const message = {
  jsonrpc: "2.0",
  id: 7,
  method: "tools/call",
  params: {
    name: intent.tool,
    arguments: intent.args,
    _meta: { "io.modelcontextprotocol/protocolVersion": "2026-07-28" },
  },
};

// Transport layer: how the bytes travel.
const asStdioLine = JSON.stringify(message) + "\n";
const asHttpPost = {
  method: "POST",
  headers: { "mcp-method": "tools/call", "mcp-name": "lookup_order" },
  body: JSON.stringify(message),
};

console.log("App wants:   ", intent.tool, JSON.stringify(intent.args));
console.log("JSON-RPC:    ", message.method, "id", message.id);
console.log(
  "stdio line:  ",
  asStdioLine.length,
  "characters, ends with a new line:",
  asStdioLine.endsWith("\n"),
);
console.log(
  "HTTP request:",
  asHttpPost.method,
  "with",
  Object.keys(asHttpPost.headers).join(" + "),
);
```

Expected output:

```text
App wants:    lookup_order {"order_id":"A-1001"}
JSON-RPC:     tools/call id 7
stdio line:   179 characters, ends with a new line: true
HTTP request: POST with mcp-method + mcp-name
```

The example showed how one tool call moves through all three layers. Next, let's look at the messages the message layer carries.

## Message Types

### Client Requests

A **request** asks for something and expects an answer. It has a `method`, an `id` and usually `params`. In the 2026-07-28 spec, the client sends all requests, such as `server/discover`, `tools/list`, `tools/call` and `resources/read`.

### Server Replies

A **response** answers one request. It carries the same `id` as the request, and either:

- a `result`, when things worked, or
- an `error`, with a number `code` and a `message`, when they didn't.

Every result also says what kind of result it is, in a field called `resultType`. Most of the time that is `"complete"`.

### Notifications

A **notification** is a message that gives an update without expecting a reply. It has a `method` but **no `id`**. Examples:

- `notifications/progress`: "I am 40% done."
- `notifications/cancelled`: "Stop working on request 12."
- `notifications/tools/list_changed`: "My tool list changed."

You can tell the three kinds apart by checking their fields:

```js runnable title="Request, response or notification?"
// Every MCP message is one of three kinds. You can tell them apart by their fields.
function kind(msg) {
  if ("method" in msg && "id" in msg) {
    return "request (expects an answer)";
  }
  if ("method" in msg) {
    return "notification (no answer)";
  }
  if ("result" in msg) {
    return "response: result";
  }
  if ("error" in msg) {
    return "response: error";
  }
  return "not a valid MCP message";
}

const seen = [
  { jsonrpc: "2.0", id: 1, method: "tools/list", params: {} },
  { jsonrpc: "2.0", id: 1, result: { tools: [], resultType: "complete" } },
  {
    jsonrpc: "2.0",
    method: "notifications/progress",
    params: { progressToken: 0, progress: 1, total: 5 },
  },
  { jsonrpc: "2.0", id: 2, error: { code: -32601, message: "Method not found" } },
  { jsonrpc: "2.0", hello: "world" },
];

for (const m of seen) {
  console.log(kind(m).padEnd(28), JSON.stringify(m).slice(0, 48));
}
```

Expected output:

```text
request (expects an answer)  {"jsonrpc":"2.0","id":1,"method":"tools/list","p
response: result             {"jsonrpc":"2.0","id":1,"result":{"tools":[],"re
notification (no answer)     {"jsonrpc":"2.0","method":"notifications/progres
response: error              {"jsonrpc":"2.0","id":2,"error":{"code":-32601,"
not a valid MCP message      {"jsonrpc":"2.0","hello":"world"}
```

For a notification sent over Streamable HTTP, the server responds with HTTP status `202 Accepted` and an empty body. The notification itself gets no JSON-RPC reply.

Check the fields to identify a message: a request has both `method` and `id`; a notification has a `method` but no `id`; and a response has an `id` plus either `result` or `error`.

Requests, responses, and notifications describe how messages move between the client and server. Some requests take longer than one exchange, so the client and server also need a way to report progress or stop the work.

## Progress and Cancellation

### Progress Updates

Some tools take time: building a report, copying files, or running tests. **Progress** messages tell the user that the tool is still working.

1. To request progress updates, the client adds a `progressToken` to the request's `_meta`.
2. While it works, the server may send `notifications/progress` with the same token, a `progress` number, an optional `total`, and an optional `message`.
3. Over HTTP, these notifications arrive on the request's own **stream**, before the final result.

```js runnable title="Turn progress messages into a progress bar"
// Progress notifications a server sent while it built a report.
const updates = [
  { progressToken: 0, progress: 1, total: 5, message: "Built part 1 of 5" },
  { progressToken: 0, progress: 2, total: 5, message: "Built part 2 of 5" },
  { progressToken: 0, progress: 4, total: 5, message: "Built part 4 of 5" },
  { progressToken: 0, progress: 5, total: 5, message: "Built part 5 of 5" },
];

for (const u of updates) {
  const pct = Math.round((u.progress / u.total) * 100);
  const bar = "#".repeat(pct / 10).padEnd(10, ".");
  console.log(`[${bar}] ${String(pct).padStart(3)}%  ${u.message}`);
}
```

Expected output:

```text
[##........]  20%  Built part 1 of 5
[####......]  40%  Built part 2 of 5
[########..]  80%  Built part 4 of 5
[##########] 100%  Built part 5 of 5
```

Watch the progress lines appear one by one. Then open the **Inspector**: that call's row says it got 5 notifications and then the result, all on one stream.

Progress updates show that a request is still running. If the user no longer wants to wait, the client can send a cancellation signal.

### Cancellation

Sometimes the user needs to stop waiting. The client sends a cancellation signal, and the server should stop the work if it can.

How the client sends that signal depends on the transport:

- **Streamable HTTP:** the client closes the stream for that request. In 2026-07-28, closing the stream is the cancel signal. No extra message is sent.
- **stdio:** there is only one shared channel, so the client sends a `notifications/cancelled` message with the `requestId`.

On the server, the SDK turns either signal into an **abort signal**, which lets the running tool know it should stop. The tool needs to check this signal and respond to it.

```js runnable title="Cancelling on each transport"
// How a client cancels a request depends on the transport.
function cancel(transport, requestId) {
  if (transport === "http") {
    return (
      "close the HTTP response stream for request " + requestId + " (no extra message)"
    );
  }
  if (transport === "stdio") {
    const note = {
      jsonrpc: "2.0",
      method: "notifications/cancelled",
      params: { requestId, reason: "user pressed stop" },
    };
    return "send " + JSON.stringify(note);
  }
  return "unknown transport";
}

console.log("Streamable HTTP:", cancel("http", 12));
console.log("stdio:          ", cancel("stdio", 12));
```

Expected output:

```text
Streamable HTTP: close the HTTP response stream for request 12 (no extra message)
stdio:           send {"jsonrpc":"2.0","method":"notifications/cancelled","params":{"requestId":12,"reason":"user pressed stop"}}
```

Try it: call `export_report` again, and press **Stop** after one or two progress lines. The Inspector shows that the stream was closed early.

A cancel is a request, not a guarantee. The server may already be done, or the tool may not check the signal. Don't assume an important action, such as a payment, was cancelled just because the client sent a cancel signal. Check the result instead.

Cancellation is a signal to stop work that is already underway. Another kind of back-and-forth happens when the server needs input before it can finish. The current spec handles this with Multi Round-Trip Requests.

## From Server Requests to Multi Round-Trip Requests

### How It Used to Work

In older versions of MCP, the **server** could also send requests to the client in the middle of its work:

- `elicitation/create`: "Ask the user this question."
- `sampling/createMessage`: "Ask your AI model to write this."
- `roots/list`: "Which folders may I see?"

This needed a long-lived, two-way connection, so the server could reach the client at any time. That made servers harder to run on the web, where each request may land on a different machine.

### How It Works Now

The 2026-07-28 spec replaced this with **Multi Round-Trip Requests**, or **MRTR**:

1. The client sends a normal request, such as `tools/call`.
2. If the server needs something first, it answers with `resultType: "input_required"` and a list of `inputRequests`, such as a request to ask the user for confirmation.
3. The client gets the answers. For example, the host shows a form to the user.
4. The client sends the **same request again**, now with `inputResponses` attached.
5. The server finishes and answers with `resultType: "complete"`.

The server does not need to call the client. Each round is a client request followed by a server response. The server can also include a `requestState` value, which the client sends back unchanged so the server can keep track of the work between rounds.

```js runnable title="An MRTR loop"
// This simplified example shows the result exchange, not full JSON-RPC messages.
function server(params) {
  const answer = params.inputResponses?.confirm;
  if (!answer) {
    return {
      resultType: "input_required",
      inputRequests: {
        confirm: {
          method: "elicitation/create",
          params: { message: "Cancel order " + params.arguments.order_id + "?" },
        },
      },
    };
  }
  if (answer.action !== "accept") {
    return {
      resultType: "complete",
      content: [{ type: "text", text: "Nothing changed." }],
    };
  }
  return {
    resultType: "complete",
    content: [
      { type: "text", text: "Order " + params.arguments.order_id + " cancelled." },
    ],
  };
}

// The client's loop: call, and if input is required, ask the user, then call again.
let params = { name: "cancel_order", arguments: { order_id: "A-1002" } };
for (let round = 1; round <= 3; round++) {
  const r = server(params);
  console.log(`round ${round}: ${r.resultType}`);
  if (r.resultType === "complete") {
    console.log(r.content[0].text);
    break;
  }
  const question = r.inputRequests.confirm.params.message;
  console.log("  user is asked:", question, "-> accepts");
  params = {
    ...params,
    inputResponses: { confirm: { action: "accept", content: { confirm: true } } },
  };
}
```

Expected output:

```text
round 1: input_required
  user is asked: Cancel order A-1002? -> accepts
round 2: complete
Order A-1002 cancelled.
```

You will build MRTR flows yourself in Chapter 5.

## Common mistakes

- **Answering a notification.** Notifications have no `id`, so there is nothing to answer. Over HTTP, the server just returns `202`.
- **Reusing an id too soon.** A client must give each open request its own id, or answers get mixed up.
- **Thinking progress is required.** It is optional. A client asks for it with a `progressToken`, and a server sends it only if it has something useful to say.
- **Sending `notifications/cancelled` over Streamable HTTP in 2026-07-28.** The cancel signal there is closing the stream.
- **Following old guides on server requests.** Sampling, elicitation and roots requests from the server were replaced by MRTR. Chapter 5 covers what is still supported.

## Exercises

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

## What to Remember

- MCP has three layers: **application** (what to do), **message** (JSON-RPC), and **transport** (stdio or Streamable HTTP).
- Messages are **requests** (method + id), **responses** (id + result or error) and **notifications** (method, no id).
- Every result has a **`resultType`**, usually `"complete"`.
- **Progress:** the client sends a `progressToken`; the server sends `notifications/progress` on the request's stream.
- **Cancel:** over HTTP, close the stream; over stdio, send `notifications/cancelled`.
- Servers no longer send requests to clients. They return **`input_required`** instead, and the client retries with **`inputResponses`** (MRTR).

Next: **JSON-RPC Messages**, every field of a request and a response, the core methods, and the error codes.
