Field guide · 14 sections · 9 diagrams
MCP, explained the way it should have been the first time
The Model Context Protocol is a small idea wearing unusually large clothes. Strip the acronym away and it is this: a standard plug so that any AI application can use any tool or data source without a custom adapter for each pairing. What follows is the full mental model — the analogies that make it click, the machinery underneath, and the places where the honest answer is "it depends".
🧪 Companion lab — build a server, then try to make it betray you →
The one-sentence version
MCP is a contract that lets an AI application discover what a service can do, then call it — in a shape every AI application already knows how to speak.
Three verbs carry the whole protocol: list what exists, read the
context you need, call the action you want. Everything else — transports,
auth, notifications — is plumbing around those three.
Two analogies, and why you need both
The USB-C analogy explains why the standard exists (fewer cables). Eli's REST analogy explains how it behaves (a request the model chooses to make). The second one is the one that will actually help you build.
The problem
Before MCP: the N×M integration tax
A useful model is a model with context. But every model vendor, every IDE and every assistant used to need its own private glue for every data source. Integrations multiply, and the multiplication is the whole problem.
Before — pairwise adapters N × M
4 AI applications × 5 data sources = 20 bespoke connectors. Each one maintained, versioned, and broken independently.
After — one protocol N + M
The same 4 applications and 5 sources need 9 implementations: each app speaks the protocol once, each source speaks it once.
The framing that makes it click
"AI is just a stack"
This is Eli's central teaching move, and it is the most useful sentence in the whole class: an LLM is one component of a stack, not the product. People say "AI" and mean the model. What actually ships is the model plus everything around it — and MCP is a piece of that "everything around it".
The LAMP stack, 20 years ago
If you were building a web app, this is the pile you needed. The pieces were not interchangeable — but they were known, and knowing them is what made you employable.
The AI stack, today
The model is layer three. Everything above and below it is also work — and this is where MCP sits: it is the shelf that tooling plugs into.
That is not a motivational aside — it is the selection criterion. MCP earns its place in the stack only if wiring a tool to a model solves someone's problem faster than not doing it.
Anatomy
Host, client, server — and why the middle one exists
Three roles. The confusing one is client, because it is not "the user" and it is not "the app" — it is a small connection object the app creates, one per server.
Host
The AI application you actually use — Claude Desktop, Claude Code, VS Code, Cursor, your own agent. It owns the conversation, decides what to show the model, and enforces the permission rules.
One host. It coordinates.
Client
A connector object inside the host, one per server, holding exactly one dedicated connection. It speaks the protocol so the host does not have to.
The host makes four clients for four servers.
Server
A program that exposes tools, resources and prompts. It can run on your laptop (stdio) or on the internet (Streamable HTTP). "Server" is not a location, it is a role.
Anything you can write in Python can be one.
The three primitives
Tools, Resources, Prompts — and who is in charge of each
This is the table worth memorising. The distinguishing question is never "what data does it expose" but who decides when it gets used.
| Primitive | What it is | Who decides | Typical methods | Eli's example |
|---|---|---|---|---|
| Tools | Executable functions with typed inputs. Can write, call, modify, trigger. | the model | tools/listtools/call |
"A tool would be a REST API call going out to weather and pulling that information back." Or ping, or system-health. |
| Resources | Read-only context addressed by URI, with a declared MIME type. Files, schemas, records. | the application | resources/listresources/readresources/templates/list |
"Resources… this is read-only information." His demo: hours://business returning opening hours. |
| Prompts | Reusable, parameterised instruction templates the server author curates. | the user | prompts/listprompts/get |
Surfaced in clients as slash commands: type / and pick one. |
Why the "who decides" column matters
It tells you where the risk lives. A tool is the only primitive that can act, and the model picks it — so a tool is where an untrusted input can turn into a side effect. Resources are read-only and the app chooses them. Prompts are inert until a human clicks one.
Rule of thumb: put capability in tools, put knowledge in resources, put repeatable judgement in prompts.
The three at a glance
What a tool actually looks like on the wire
A tool is a name, a description and a JSON Schema. The model never sees your Python — it sees this.
{
"name": "searchFlights",
"description": "Search for available flights",
"inputSchema": {
"type": "object",
"properties": {
"origin": { "type": "string", "description": "Departure city" },
"destination": { "type": "string", "description": "Arrival city" },
"date": { "type": "string", "format": "date" }
},
"required": ["origin", "destination", "date"]
}
}
That description string is doing more work than it looks like. It is the only thing the model reads when deciding whether to call your tool — which is why "tool poisoning" (see Security) is a real attack class, and why Eli's class spends time on rewriting descriptions when you swap models.
The conversation
What actually happens when you ask a question
Five messages, then an answer. The part beginners miss: the model does not call your tool. It asks the host to, and the host decides whether to obey.
The handshake, as Eli describes it
Version and capability negotiation. This is the step that lets an old client talk to a new server without either one guessing.
The same exchange as JSON-RPC
{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "checkWeather",
"arguments": { "city": "Paris" }
}
}
Every method in the protocol is just a method string on this envelope. There is no second protocol to learn.
Transports
Two pipes, one message format
The transport is how bytes get from client to server. It changes deployment, not semantics — the same JSON-RPC goes down either pipe.
| stdio | Streamable HTTP | |
|---|---|---|
| How it connects | Host launches the server as a child process; messages over stdin/stdout | HTTP POST for client→server; optional Server-Sent Events for streaming back |
| Where it runs | Same machine as the host | Anywhere — another container, another continent |
| Who it serves | Typically a single client | Typically many clients |
| Auth | None needed — it inherits the user's own privileges | Bearer tokens, API keys, custom headers; OAuth recommended |
| Latency | No network hop | Network hop + TLS + auth |
| Right for | Local tools: files, shell, local DB, system health | Shared services: SaaS APIs, team servers, hosted tooling |
The security consequence of the choice
A stdio server is you — same filesystem, same credentials, same rights. A remote server is someone else's program with its own tokens, and now every question about trust, scope and revocation applies. Most MCP security incidents in the wild live on the remote side of this line.
The code
A server is a function with a docstring
The SDK's own words: you write no JSON Schema, no request parsing, no validation code, no protocol handling. Two type-hinted Python functions and a docstring. Everything below is lifted from Eli's class repository and the official SDK.
mcp-server-basic.pyPythonfrom datetime import datetime from mcp.server.mcpserver import MCPServer mcp = MCPServer("time-server") @mcp.tool() def get_time() -> str: """Return the current time (HH:MM:SS).""" return datetime.now().strftime("%H:%M:%S") @mcp.tool() def get_day_month_year() -> str: """Return the current day of the week.""" return datetime.now().strftime("%A, %B, %Y") if __name__ == "__main__": mcp.run()
The decorator IS the schema declaration: () -> str tells the protocol the signature,
the docstring becomes the description the model reads.
mcp-server-resource-hours.pyPythonfrom mcp.server.mcpserver import MCPServer mcp = MCPServer("Business Hours Server") @mcp.resource("hours://business") def get_business_hours() -> str: """Provides the current hours of operation.""" return ( "Store Hours of Operation:\n" "Monday - Friday: 9:00 AM - 6:00 PM\n" "Saturday: 10:00 AM - 4:00 PM\n" "Sunday: Closed" ) if __name__ == "__main__": mcp.run()
Note the URI scheme. hours://business is addressed like a document, because it is
one — read-only, no arguments, the application decides whether to pull it in.
And a tool that actually does something
From Eli's mcp-server-ping.py — the shape of every useful tool: validate the input,
do the work, return a string. Here the agent gets network tools without ever holding a shell.
IPV4_PATTERN = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b") @mcp.tool() def extract_ip(text: str) -> str: """Find and return the first valid IPv4 address in a block of text.""" for candidate in IPV4_PATTERN.findall(text): try: ipaddress.IPv4Address(candidate) # rejects things like 999.1.1.1 return candidate except ValueError: continue return "Error: no valid IPv4 address found in the text."
That validation line is the whole argument for MCP over "just give the model a shell". You expose one bounded capability, not a terminal.
The other end: a client in ten lines
import asyncio from mcp import Client async def main() -> None: async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool("add", {"a": 1, "b": 2}) print(result.structured_content) # {'result': 3} asyncio.run(main())
A URL means Streamable HTTP. The same Client can launch a local server as a stdio
subprocess instead. That is the entire local-vs-remote difference, from the caller's side.
The map is not the territory
Where the spec has moved since the class
An honest guide has to say this: the mental model you learn from a class filmed in October 2026 and the current specification are not the same document. The concepts below are stable; the wire has moved. Here is what changed, so a tutorial does not mislead you later.
What you learn in the class (and should keep)
- Host / client / server roles
- Three primitives, and who controls each
- A handshake that negotiates version + capabilities
- stdio for local, HTTP for remote
- Anything you can code in Python can become a server
- Human approval before a tool runs
None of this is wrong. It is the vocabulary you will use for years.
What the 2026-07-28 spec now says
- Stateless. Every request carries the protocol version and the caller's capabilities in its
_metafield — the server processes each request on its own. server/discoveris the mandatory request where a server advertises supported versions and capabilities. Clients may send it before anything else.- Notifications became subscriptions. A client opens
subscriptions/listenwith a filter (e.g.toolsListChanged: true) and gets tagged events on that stream. - Sampling is deprecated as of this protocol version.
Security
The part where you stop trusting the plumbing
MCP gives a model the ability to act. Everything interesting and everything dangerous follows from that. The official security guidance is not vague about this; here are the real named failures.
1 · Confused deputy
An MCP proxy server holds one static client ID with a third-party API. It lets clients register dynamically, and the third party drops a consent cookie after the first approval. An attacker then sends you a link with their redirect URI; your browser still has the consent cookie, so the consent screen is skipped and the code lands on attacker.com
Mitigation: per-client consent stored per user, exact-match redirect URIs, single-use state issued only after consent.
2 · Token passthrough
A server accepts a token from a client without verifying it was issued to that server, then passes it downstream. The token's audience is never checked, so a token minted for one service becomes a key to another.
Mitigation: validate audience on every token; never forward a client's token as your own credential.
3 · Omnibus scopes
The spec's own "common mistakes" list is blunt: wildcard scopes like *,
all, full-access, and publishing your entire scope catalog in every
challenge. Consent fatigue then trains users to click approve.
Mitigation: request the scopes the operation needs, log elevation events, let users down-scope.
4 · The one that follows from the design: poisoned tool descriptions
Not from the OAuth section — from the primitives. The model decides whether to call a tool by reading its description string. That string is attacker-controlled if the server is. A malicious or compromised server can therefore ship a tool whose description steers the model, and no amount of schema validation catches it, because the schema is valid.
Which is why the approval prompt must show what will run and with what arguments, not just a tool name — and why "allow always" is the setting that converts one bad click into a standing grant.
Four lenses, one subject
Ask the same protocol four different questions
A single explainer serves one reader. This is the same material interrogated from the perspectives that actually decide whether a thing gets adopted — learner, builder, security reviewer, architect.
"What do I actually have to remember?"
Three roles, three primitives, two transports, one envelope. If you can say "the host makes one client per server; the model picks a tool and the host runs it; tools act, resources inform, prompts wait", you know more than most people deploying this.
Common misconception to drop: MCP does not make the model smarter, and it does not give the model memory. It gives it reach.
"Where will my afternoon go?"
Not into the protocol. The SDK removes it: a decorator and a docstring. Your afternoon goes into tool design — naming, granularity, error text the model can act on, and the schemas that stop it calling your tool with nonsense.
A tool that returns "Error" teaches the model nothing. A tool that returns "no valid IPv4 in the text, expected dotted-quad" teaches it to retry correctly.
"What can this reach that it shouldn't?"
A stdio server inherits your whole user account. A remote server brings tokens you can't revoke locally. Every tool is an action surface, and the model choosing it means an untrusted document can influence which action fires.
The questions to ask: what is the blast radius of this one tool, who can change its description, and is "always allow" ever on?
"Why not just an API?"
Often you should. A REST API with a typed client is simpler, cacheable and debuggable. MCP pays off when many hosts must speak to many servers and you refuse to write the glue twice — and when you want discovery at runtime instead of a compile-time dependency.
If exactly one application talks to exactly one service, MCP is ceremony. (See the next section.)
Judgement
When MCP is the wrong answer
A guide that only argues for its subject is marketing. These are the cases where plain code wins, and saying so is the difference between a protocol and a religion.
Use plain function calling or a REST client when…
- One app, one service. You are writing a bespoke integration anyway; a typed API client is less machinery.
- The tool set is fixed at build time. Runtime discovery buys you nothing if the list never changes.
- Latency is the product. An extra protocol hop and a schema round-trip cost milliseconds that a chat UI never notices and a trading loop always does.
- You need caching, retries or batching as first-class features. HTTP gives those to you; MCP does not.
Reach for MCP when…
- Many hosts × many sources. The N+M argument is the entire point.
- You want one server to serve Claude, an IDE and your own agent.
- The capability should be discoverable at runtime — tools that come and go with permissions, not a pinned client library.
- You want a bounded capability instead of a shell. "Here is a tool that validates an IP address", not "here is root".
The one-line test
Would you write this integration more than once, for a tool you don't control? If yes — MCP. If no — a function call.
Check yourself
Eight questions that prove you understood
Click a card to reveal the answer. If any of these surprise you, the section above it is worth a second read.
Provenance
Sources
Everything factual above traces to one of these. Quotations are verbatim from the transcript; diagrams are redrawn from the concepts, not copied.
- Eli the Computer Guy — Intro to MCP: Model Context Protocol for AI Tool Use (published 17 Oct 2026, 31,806 views). youtube.com/watch?v=TXrZ0ymjbZs — the class this guide is built around. Transcript obtained via youtubetotranscript.com (YouTube blocks this host's IP range directly).
- Silicon Dojo — class workbook and code samples,
MCP - Model Context Protocol/. github.com/SiliconDojo/In-Person-Classes — the time server, ping tools and hours resource code above are his. - MCP documentation — What is the Model Context Protocol. modelcontextprotocol.io/docs/getting-started/intro — the USB-C analogy, and the list of clients that support MCP.
- MCP documentation — Architecture overview. modelcontextprotocol.io/docs/learn/architecture — participants, the two layers, statelessness, primitives, subscriptions.
- MCP documentation — Understanding MCP servers. modelcontextprotocol.io/docs/learn/server-concepts — the tools/resources/prompts table including "who controls it", and the protocol operations.
- MCP documentation — Security best practices (protocol 2026-07-28). modelcontextprotocol.io/…/security_best_practices — confused deputy, token passthrough, consent and scope requirements.
- MCP Python SDK (MIT). github.com/modelcontextprotocol/python-sdk — the 15-line server and 10-line client, and the "what you did not write" framing.
- Anthropic — Introducing the Model Context Protocol. anthropic.com/news/model-context-protocol — the origin: open-sourced, created by David Soria Parra and Justin Spahr-Summers, early adopters Block and Apollo.