Open Claude Desktop's config file, add five lines of JSON naming a command to run, restart the app, and a paperclip icon appears: the model can suddenly search your notes, and it asks your permission before deleting one. Nothing about the model changed. What happened is that the app spawned your command as a subprocess, exchanged three JSON messages with it, asked it what it could do, and folded the answer into the model's context. Those three messages, and the roles of the two parties exchanging them, are the whole architecture of MCP — everything else in this section of the course is detail hanging off them.
This lesson gives you the architectural map you will build against for the next three lessons: the host/client/server split and why it exists, the JSON-RPC framing underneath, the version-and-capability handshake, the three server primitives (tools, resources, prompts), and the three server-initiated flows (sampling, elicitation, roots). By the end you should be able to look at any integration idea and say which primitive it belongs to — a design decision that matters more than any line of server code you will write.
MCP names three roles. The host is the application the user actually runs — Claude Desktop, Cursor, VS Code, or the agent you wrote yourself. The server is the program exposing capabilities: your notes store, a GitHub wrapper, a database gateway. Between them sits the client, a component inside the host that maintains exactly one stateful connection to exactly one server. A host connected to five servers runs five clients.
The host-client split looks like pedantry until you ask where
policy lives. The client is deliberately dumb plumbing: it speaks
the wire protocol, tracks one session, forwards messages. The
host is where everything trust-shaped happens: it decides which
servers to connect at all, merges their tool lists under distinct
names so two servers' search tools cannot collide or
impersonate each other, holds the conversation and the model, and
gates every sensitive action behind user consent. Servers never
see the conversation, never see each other, and never talk to the
model directly — every request a server makes is mediated by the
host, which can rewrite, deny, or surface it to the user.
Underneath, every MCP message is JSON-RPC 2.0 — a 2010-era
remote-procedure format chosen precisely because it is boring:
three message shapes and nothing else. A request carries
an id, a method, and params,
and demands exactly one response. A response echoes the
id with either a result or an
error. A notification has a method but no
id — fire and forget, no reply permitted. Calling a
tool looks like this on the wire:
{"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": "add_task", "arguments": {"title": "Ship the Q3 report"}}}
{"jsonrpc": "2.0", "id": 2, "result": {
"content": [{"type": "text", "text": "Created task 17: Ship the Q3 report"}],
"isError": false}}
Two details repay attention. First, both sides can send requests:
the client calls tools/call on the server, but the
server can call sampling/createMessage or
elicitation/create on the client — MCP is
bidirectional, which is what makes the server-initiated flows in
Section 5 possible. Second, notice isError inside a
successful JSON-RPC response. Protocol errors (unknown method,
malformed params) use the JSON-RPC error object, but
a tool that ran and failed — file not found, API returned 403 —
reports failure in-band as result content, so the model
can read the message and try something else. Confusing these two
error channels is the most common first-week server bug: raise a
transport-level error for a business-level failure and the host
may retry or disconnect instead of letting the model recover.
Every session opens the same way. The client sends
initialize, proposing a protocol revision and
declaring what it supports; the server answers with the revision
it will speak and its own capability set; the client acknowledges
with a notification. The three messages from the Claude Desktop
story, concretely:
{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {
"protocolVersion": "2025-11-25",
"capabilities": {"roots": {"listChanged": true}, "sampling": {}, "elicitation": {}},
"clientInfo": {"name": "my-agent", "version": "0.1.0"}}}
{"jsonrpc": "2.0", "id": 1, "result": {
"protocolVersion": "2025-11-25",
"capabilities": {"tools": {"listChanged": true},
"resources": {"subscribe": true, "listChanged": true},
"prompts": {}},
"serverInfo": {"name": "tasks-server", "version": "1.2.0"}}}
{"jsonrpc": "2.0", "method": "notifications/initialized"}
Version negotiation is blunt on purpose: the client proposes the
newest revision it speaks; if the server supports it, it echoes
it back, otherwise it answers with the newest it does support,
and the client either proceeds at that level or disconnects.
Capability negotiation is finer-grained. Nothing is assumed:
a server that never declared resources must never be
sent resources/list, and a server must not attempt
elicitation against a client that did not declare it. Sub-flags
matter too — listChanged means 'I will notify you
when my list changes', and subscribe means 'you may
subscribe to updates on individual resources'. Well-behaved code
checks the negotiated capability set and degrades: if the client
lacks elicitation, your server should fail informatively instead
of asking questions into the void.
Everything a server offers is one of three primitives, and the spec distinguishes them by an unusual axis: who decides when they are used.
| Primitive | Controlled by | Semantics | Examples |
|---|---|---|---|
| Tool | The model | An operation the model chooses to invoke to accomplish a goal; may have side effects | add_task, search_issues,
send_email |
| Resource | The application (host) | Addressable data, identified by URI, read without side effects; context the host attaches | tasks://open, file:///readme.md,
db://schema/orders |
| Prompt | The user | A named, parameterized message template the user picks explicitly (slash command, menu item) | /weekly-review, /summarize-thread |
The control axis is the design test. A tool is something you would let the model decide to do mid-reasoning. A resource is something a host might list, let the user pick, or attach to context automatically — like a file the user drags into the chat; the model does not fetch it, the application supplies it. A prompt is a workflow the user triggers deliberately, with the server owning the wording. All three travel over the same JSON-RPC session; they differ in who pulls the trigger.
The reverse direction is what separates MCP from 'OpenAPI with extra steps'. Three flows let the server ask for things:
Sampling — the server asks the host's model to
complete a prompt (sampling/createMessage). Your
tasks server wants to auto-summarize a hundred completed tasks:
instead of embedding its own model API key, choosing a vendor,
and billing separately, it sends the text to the client and asks
the host to run an LLM call on its behalf. The host controls
which model, applies rate limits, and can require user approval
per request. Intelligence flows from host to server; the server
stays a thin, keyless program.
Elicitation (since revision 2025-06-18) — the server asks the user a structured question mid-operation: a confirmation before a destructive step, a missing parameter, a choice among ambiguous matches. The request carries a flat JSON schema; the host renders a form and returns the user's answer — or their refusal. Lesson 4 builds real elicitation flows and the judgment call of when to elicit versus when to fail.
Roots — the client tells the server which
filesystem directories or URI prefixes are in scope
(roots/list), and notifies when the set changes. A
filesystem server connected to your IDE learns 'you operate
within ~/projects/api' and can decline to wander
outside it. Roots are guidance, not enforcement — a boundary the
polite server respects and the host's sandbox actually enforces,
a layering you saw from the other side in Tool Use, Code
Execution & Sandboxing.
Check: your server wraps a documentation wiki. The host should be able to attach the style guide to context when the user opens a writing session; the model should be able to search pages while reasoning; the user wants a one-click 'draft release notes' action. Which mapping is right?
The classification sounds clean until you meet a real case, so
here are the edges. Read-only operations are not
automatically resources. A parameterized search with ranking
logic is a tool — the model decides when and with what query.
A resource is for data with a stable address: tasks://17,
today's log, the schema of a table. If you find yourself wanting a
resource URI with six query parameters, you want a tool.
Write operations are never resources or prompts. And a
prompt is not a tool the user calls — it produces messages
for the model, not effects; 'draft release notes from these
commits' is a prompt, 'publish release notes' is a tool.
A useful smell test from real 2026 servers: if your tool list
includes get_config, get_readme, or
get_schema — no-argument getters returning static
documents — you have mislabeled resources as tools, and you are
paying for it in model behavior: every fetch costs a reasoning
turn and a tool-call round trip, and hosts cannot prefetch or
cache what they cannot address. Ten no-arg getters also crowd the
tool list, and tool-selection accuracy degrades measurably as
the list grows past a couple of dozen entries.
Hold the whole architecture in one paragraph: a host runs one client per server; each session opens with a version-and- capability handshake and then speaks JSON-RPC both ways. Servers offer tools (model-controlled), resources (application-controlled), and prompts (user-controlled); servers reach back through sampling (borrow the host's model), elicitation (ask the user), and roots (learn their boundaries). Policy, consent, and the model live in the host; capability lives in the server; the client is deliberately boring plumbing.
Next lesson you stop drawing maps and start building: a working MCP server over a real task store, wired into the MCP Inspector, Claude Desktop, and the agent loop you built two courses ago.