Agents, lesson 15
MCP end to end
The messages between an agent and its tools, and how they get attacked.
Many agents reach their tools through MCP, so when a call goes wrong you read its messages. "Designing tools a model can use" showed tools/list and tools/call, and "The harness around a coding agent" showed what a server can reach. Here you follow the messages, in the current spec, and the attacks on them.
By the end, you can:
- Trace one tool call between the model, the host and a server.
- Read a request in the current spec, and say how it reaches a local or remote server.
- Follow a call that stops to ask the user, and say what each kind of ask risks.
- Spot a poisoned, changed or shadowing tool description, and name the host's defences.
Part 1. Four parties, one tool call
Trace one tool call between the model, the host and a server.
You ask a chat app with a GitHub MCP server to "file a bug: the export button does nothing in Safari". Four parties take part: you, the app, the model behind it and the server. Follow the messages: the model and the server never talk.
Example 1.1
One bug report, message by message
The app has one MCP server, offering create_issue(repo, title). Count the messages between the app, the model and the server, from connecting until you read the reply. How many are MCP?
- List the toolsapp → server: tools/list, and its response
Sent before you type a word. The response holds each tool's name, description and schema.
- Offer the toolapp → model: your message + the tool's definition
Over the model provider's own tool-calling API, not MCP. The model never learns which server it came from.
- Let the model pickmodel → app: create_issue("acme/web", …)
Only a proposal. The app can check your permissions, or ask you.
- Call the serverapp → server: tools/call, and its response
The server opens the issue: "Created acme/web#482".
- Hand back the resultapp → model: result; model → app: reply
The model writes "Done, I filed #482."
Answer8 messages, and 4 are MCP: a request and a response for tools/list, and the same for tools/call.
{"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": "create_issue",
"arguments": {"repo": "acme/web", "title": "Export button does nothing in Safari"},
"_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}}}}
{"jsonrpc": "2.0", "id": 2,
"result": {"resultType": "complete", "isError": false,
"content": [{"type": "text", "text": "Created acme/web#482"}]}}That split is the point. The app turns each server's tool definitions into its model's format, and the model's choice back into tools/call, so one server works with any app and any model. The server sees only its own calls, never the conversation.
Host, client and server
The host is the app you use: a chat app, an IDE, an agent. It holds the conversation, calls the model, and runs one MCP client per server it connects to. A server offers tools, resources and prompts. The model only talks to the host.
model ↔ host (one client per server) ↔ server
Failures come back two ways. A tool the server doesn't have gets a JSON-RPC error. A tool that ran and failed, say on a past date, returns a result with isError: true, which the host passes to the model to fix its call.
Your turn
The app already has the tool list. You ask it to comment on the Safari bug that it's fixed. The model calls search_issues, then add_comment. How many MCP messages are sent, and how many times is the model called?
tools/call is a request and a response: 2 × 2 = 4. The model picks search_issues, then add_comment, then writes the reply: 3 calls. 6 lists the tools again.Part 2. Every request stands alone
Read a request in the current spec, and say how it reaches a local or remote server.
Keep going with Pro
You've done the first part. Pro opens the rest of this lesson, and every other one.
- Every request stands alone
- When the server needs more
- How it gets attacked
Agents overviewSpotted a mistake? Tell us.