# MCP Mental Model

Course: [Model Context Protocol](https://www.devtutorials.ai/learn/mcp) · Section: Foundations
Source: https://www.devtutorials.ai/learn/mcp/foundations/mcp-mental-model

> The five roles in every MCP setup (user, host, client, server, backend), why each server gets its own client, and the main duties of hosts and servers.

Imagine you are ordering food at a restaurant. You talk to the waiter. The waiter takes your order to the kitchen. The kitchen uses the fridge and stove to cook your food. Then the food comes back to you the same way.

You never go into the kitchen. The cook never comes to your table. Each person has one job, and they pass things along in a set order.

MCP works like this too. There are five roles, and each one has a clear job. Once you know the roles, every MCP setup will make sense.

**The main idea:** In MCP, the **user** talks to a **host**. The host runs one **client** for each **server**. Each server connects to one **backend system**.

## A Real-World Example

Brightgrid sells home products. Its support team uses an AI assistant.

- Maya, a support agent, types: "Where is order A-1002?"
- The assistant app sends the question to its AI model.
- The model asks to use the `lookup_order` tool.
- The app's MCP client sends that request to Brightgrid's help-desk MCP server.
- The server asks the order database, then sends back: "Air purifier, processing, arrives Oct 18."
- The model writes a friendly answer for Maya.

Maya is the user. The assistant app is the host. The help-desk server sits in front of the order database, the backend.

**In simple words:** The user asks, the host decides, the client carries the request, the server does the work, and the backend holds the data.

## What You Will Learn

In this lesson, you will learn:

1. The five roles in every MCP setup.
2. Why each server gets its own client.
3. The four main jobs of a host.
4. The four main jobs of a server.

The **MCP Lab** at the bottom of the page acts as the host. It runs one client connected to one server: Brightgrid's help desk. You can see the server's code in the **Server code** tab.

## The Five Roles

### User

The **user** is the person using the AI app. The user asks questions and approves risky actions. The user never talks to an MCP server directly.

### Host

The **host** is the AI app the user opens: a chat app, a code editor, or a support tool. The host:

- Holds the AI model, or talks to it.
- Creates and manages the MCP clients.
- Decides what goes into the model's context.
- Asks the user before risky actions.

The host is the boss of the setup. It is the only part that sees everything.

### Client

A **client** is a small part inside the host. Each client connects to **only one** server. It sends requests to that server and brings back the answers.

Why one client per server? It keeps servers separate. The orders server never sees what the wiki server sent. If one server fails or acts badly, only its own client is affected.

```js runnable title="One client for each server"
// One host, two servers. The host makes one client for each server.
const host = {
  name: "Brightgrid Assistant",
  clients: [
    { id: "client-1", server: "helpdesk", system: "Orders and tickets" },
    { id: "client-2", server: "wiki", system: "Company wiki" },
  ],
};

console.log("User opens:", host.name, "(the host)");
for (const c of host.clients) {
  console.log(`  ${c.id} <-> server "${c.server}" <-> ${c.system}`);
}
console.log(
  "Clients per server:",
  host.clients.length / new Set(host.clients.map((c) => c.server)).size,
);
```

Expected output:

```text
User opens: Brightgrid Assistant (the host)
  client-1 <-> server "helpdesk" <-> Orders and tickets
  client-2 <-> server "wiki" <-> Company wiki
Clients per server: 1
```

### Server

A **server** is a program that gives AI apps access to one system. It says what it offers, checks each request, and does the work. A server can be:

- **Local:** a small program on your own computer, like one that reads your files.
- **Remote:** a service on the internet, like a company's help-desk server.

A server does not see the model. It doesn't know which model is used or what the user typed. It only sees the requests its client sends.

### Backend Systems

The **backend** is the real system behind the server: a database, a ticket system, a file folder, or another company's API. The backend doesn't know about MCP at all. The server translates between MCP and the backend.

A good way to remember the order: **U-H-C-S-B**. User, Host, Client, Server, Backend. Requests flow left to right. Answers flow back right to left.

## Host Duties

### Client Supervision

The host starts each client, connects it to its server, and closes it when it is no longer needed. If a server stops answering, the host notices and can tell the user or try again. The host also decides which servers to connect to. A user or an admin usually picks them in the host's settings.

### Context Aggregation

The model needs one clear list of tools. So the host **collects** the tool lists from every client and merges them into one list for the model. It does the same with resources it decides to include.

Two servers can offer tools with the same name, like `search`. The host must keep them apart, for example by adding the server name.

```js runnable title="Merge tool lists from two servers"
// Context aggregation: the host merges tool lists from two servers.
// It adds the server name, so two tools called "search" can't be mixed up.
const lists = {
  helpdesk: [{ name: "lookup_order" }, { name: "search" }],
  wiki: [{ name: "search" }, { name: "get_page" }],
};

const merged = [];
for (const [server, tools] of Object.entries(lists)) {
  for (const t of tools) {
    merged.push({ id: `${server}.${t.name}`, server, tool: t.name });
  }
}

console.log(merged.map((m) => m.id).join("\n"));
console.log("Total tools shown to the model:", merged.length);
```

Expected output:

```text
helpdesk.lookup_order
helpdesk.search
wiki.search
wiki.get_page
Total tools shown to the model: 4
```

### Approval Gates

Some tools only read. Others change things: they open tickets, send emails, or give refunds. The host puts an **approval gate** in front of risky tools. Before the call runs, it shows the user what will happen and waits for "yes".

A server can mark a tool with **annotations**, like `readOnlyHint: true`. A careful host treats a tool with no hints as risky.

```js runnable title="A simple approval gate"
// A host's approval gate: read-only tools run at once, others ask the user first.
function needsApproval(tool) {
  return tool.annotations?.readOnlyHint !== true;
}

const tools = [
  { name: "lookup_order", annotations: { readOnlyHint: true } },
  { name: "create_ticket", annotations: { readOnlyHint: false } },
  { name: "refund_order" }, // no hints at all
];

for (const t of tools) {
  console.log(t.name.padEnd(14), needsApproval(t) ? "ask the user first" : "run now");
}
```

Expected output:

```text
lookup_order   run now
create_ticket  ask the user first
refund_order   ask the user first
```

In the lab, the Explorer acts like a developer's tool: it calls tools straight away. A real host would ask Maya first, because `create_ticket` is not marked read-only.

A server can also ask the user something itself, in the middle of a tool call. The `cancel_order` tool does this. Its first answer is not a result but `resultType: "input_required"`, with a question inside. The lab shows the question as a form. When you answer, the client sends the same request again with your answer attached. You will learn this pattern, called a **Multi Round-Trip Request**, in Chapter 5.

### Security Policy

The host enforces the rules that keep users safe:

- Only connect to servers the user or admin trusts.
- Don't send user data to a server without the user agreeing.
- Keep a log of every tool call, so people can check later what happened.
- Treat text that comes back from a server as data, not as instructions for the model.

## Server Duties

### Capability Declarations

When a client asks, the server says what it can do. In the 2026-07-28 spec, the client's first request is `server/discover`. The server answers with the spec versions it supports and its **capabilities**: whether it offers tools, resources, prompts, or all three.

Then the client asks for the lists with `tools/list`, `resources/list`, and `prompts/list`. A server only answers for what it declared.

### URI Resolution

Every resource has a **URI**, like `kb://returns-policy`. When a client sends `resources/read` with a URI, the server must find the right data and return it. If no resource has that URI, the server sends an error. In the 2026-07-28 spec, that error uses code `-32602`.

### Input Schemas

Every tool has an **input schema**. It lists the arguments, their types, and which ones are required. The server publishes the schema, and it must also **check** every call against it. Never trust that the arguments are right just because a model made them.

```js runnable title="Check arguments against the schema"
// A server checks arguments against the tool's input schema before it runs anything.
const schema = {
  type: "object",
  properties: {
    summary: { type: "string" },
    priority: { type: "string", enum: ["low", "high"] },
  },
  required: ["summary", "priority"],
};

function check(args) {
  const problems = [];
  for (const key of schema.required) {
    if (!(key in args)) {
      problems.push(`missing ${key}`);
    }
  }
  for (const [key, value] of Object.entries(args)) {
    const rule = schema.properties[key];
    if (!rule) {
      problems.push(`unknown ${key}`);
      continue;
    }
    if (typeof value !== rule.type) {
      problems.push(`${key} must be a ${rule.type}`);
    } else if (rule.enum && !rule.enum.includes(value)) {
      problems.push(`${key} must be one of ${rule.enum.join("/")}`);
    }
  }
  return problems.length ? problems.join("; ") : "ok";
}

console.log(check({ summary: "Lamp broken", priority: "high" }));
console.log(check({ summary: "Lamp broken" }));
console.log(check({ summary: "Lamp broken", priority: "urgent", color: "red" }));
```

Expected output:

```text
ok
missing priority
priority must be one of low/high; unknown color
```

Here the number `1001` is not text, so the server refuses. Look at the reply: it is a normal result with `isError: true`, and the message says what is wrong. The model can read it and try again.

### Tool Execution

When the arguments pass, the server runs the tool and returns a result. The result holds **content**, like a piece of text. If the work fails, the server says so in the result with `isError: true`. If the request itself makes no sense, like an unknown method, the server returns a JSON-RPC **error** instead.

```js runnable title="A server answering four requests"
// A tiny server's job: answer methods it knows, report errors for the rest.
const tools = {
  lookup_order: ({ order_id }) => `${order_id}: Desk lamp, shipped, arrives Oct 14`,
};

function handle(req) {
  if (req.method === "tools/list") {
    return {
      id: req.id,
      result: {
        tools: Object.keys(tools).map((name) => ({ name })),
        resultType: "complete",
      },
    };
  }
  if (req.method === "tools/call") {
    const fn = tools[req.params.name];
    if (!fn) {
      return {
        id: req.id,
        error: { code: -32602, message: `Tool ${req.params.name} not found` },
      };
    }
    return {
      id: req.id,
      result: {
        content: [{ type: "text", text: fn(req.params.arguments) }],
        resultType: "complete",
      },
    };
  }
  return { id: req.id, error: { code: -32601, message: "Method not found" } };
}

console.log(JSON.stringify(handle({ id: 1, method: "tools/list" })));
console.log(
  JSON.stringify(
    handle({
      id: 2,
      method: "tools/call",
      params: { name: "lookup_order", arguments: { order_id: "A-1001" } },
    }),
  ),
);
console.log(
  JSON.stringify(
    handle({
      id: 3,
      method: "tools/call",
      params: { name: "delete_all", arguments: {} },
    }),
  ),
);
console.log(JSON.stringify(handle({ id: 4, method: "weather/today" })));
```

Expected output:

```text
{"id":1,"result":{"tools":[{"name":"lookup_order"}],"resultType":"complete"}}
{"id":2,"result":{"content":[{"type":"text","text":"A-1001: Desk lamp, shipped, arrives Oct 14"}],"resultType":"complete"}}
{"id":3,"error":{"code":-32602,"message":"Tool delete_all not found"}}
{"id":4,"error":{"code":-32601,"message":"Method not found"}}
```

Never run a tool before checking its arguments. A model can produce wrong or even harmful arguments, and a user can trick a model into it. The server is the last line of defense in front of the backend.

## Common mistakes

- **Thinking the server talks to the model.** The server only talks to its client. The host passes things to and from the model.
- **Thinking the user talks to the server.** The user only talks to the host.
- **Putting approval logic in the model's prompt.** "Ask before refunds" in a prompt is not a gate. The host must block the call in code until the user agrees.
- **Skipping server-side checks.** The client may check too, but the server must always check, because it protects the backend.
- **Confusing errors.** A failed tool run is a result with `isError: true`. An unknown method is a JSON-RPC error.

## Exercises

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

## What to Remember

- Every MCP setup has five roles: **user, host, client, server, backend**.
- The **host** holds the model and runs **one client per server**.
- Host duties: **supervise clients**, **merge context**, **gate risky tools**, and **enforce security policy**.
- Server duties: **declare capabilities**, **resolve URIs**, **publish and check input schemas**, and **run tools**.
- A failed tool run returns `isError: true`. An unknown method returns a JSON-RPC **error**.

Next: **Connected Protocol Workflows**, how a request travels from discovery to a finished answer, and why that beats custom code.
