Model Context Protocol
The universal standard that connects AI models to any data source or tool. Build once. Plug in everywhere. Six concepts, from "why MCP exists" to a production server topology.
The 6 concepts of MCP
Tap any concept to jump to its 5-card cluster.
- 1What Is MCP?USB-C for AI — one protocol, every tool
- 2MCP ArchitectureClient, server, and the three primitives
- 3Build Your First ServerTools, resources, prompts in one file
- 4Connect ClientsWire Claude Desktop with one config entry
- 5Resources vs. Tools vs. PromptsPick the right primitive every time
- 6Production EcosystemCanonical servers, least-privilege, skill vs MCP
USB-C for AI tools
Before MCP, every AI model had its own way of plugging into every tool. Three models × ten tools meant 30 custom integrations, each different, each fragile. Add a model and you rebuild ten connectors. Add a tool and you rebuild three.
MCP collapses that N×M into N+M. Each model speaks MCP once. Each tool speaks MCP once. They interoperate automatically. The protocol is open, JSON-RPC based, and not Anthropic-specific — the same MCP server works with Claude, Cursor, Zed, or any compatible host.
The phone-charger drawer
BEFORE: Picture the drawer of cables most of us had a decade ago — Micro-USB, Lightning, Mini-USB, barrel jacks, proprietary plugs from every laptop maker. Every device shipped with its own cord, and a friend's charger usually didn't fit your phone.
PAIN: Travel with three devices and you packed three chargers, three bricks, three sets of adapters. Lose one and you bought a fourth cable that worked only with that exact device. The integration cost lived in your bag for years.
MAPPING: USB-C ended that. One plug shape. Any cable, any device, any port. MCP is that for AI: one protocol shape between any model (host) and any tool (server). Plug in a new server, every host can use it tomorrow.
What's actually on the wire
Strip the buzzwords away and MCP is three small ideas stitched together. Every conversation between host and server uses these:
- JSON-RPC 2.0 messagesEach call is a tiny JSON object: a method name (
tools/call,resources/read), some parameters, an id. The server replies with a result or an error using the same id. - Capability negotiationThe very first exchange isn't real work — it's the host asking "what do you support?" and the server answering with a list of tools, resources, and prompts. No SDK is required to know what's there.
- Transport choiceThe same JSON travels over either stdio (host launches the server as a local subprocess and pipes JSON over stdin/stdout) or HTTP+SSE (server lives on a network endpoint, client uses HTTP requests + Server-Sent Events).
- The host is in chargeThe model never talks to the network directly. The host (Claude Desktop, Claude Code, Cursor) controls which servers are connected, mediates every call, and presents results back to the model.
That's the whole protocol surface. Everything else — tool schemas, resource URIs, prompt templates — is just data riding on top of it.
When to reach for MCP
# Question: should this integration be MCP? IF capability is needed by >1 host: # Claude Desktop AND Cursor AND your app USE MCP # write once, every host gets it ELIF capability touches live state: # DB rows, files, Slack, GitHub PRs USE MCP # skills can't fetch live data ELIF capability is a workflow / pattern: # "here is how we deploy to k8s" USE a Skill # markdown is auditable ELIF single-app, single-call function: USE raw tool_use (Module 5) # MCP overhead not justified ELSE: START with raw tools, refactor later
Default rule: build it as MCP the moment a second host wants the same capability. That's when N+M starts paying for itself.
Misconceptions
MCP is the standardized cable between AI hosts and external systems. Open protocol, JSON-RPC on the wire, two transports (stdio + HTTP+SSE), three primitives.
If you remember nothing else: N+M, not N×M. Build a capability once as an MCP server and every compatible host can use it tomorrow.
Two parties, three primitives
An MCP system has exactly two roles. The client (also called the host) is the AI-facing app: Claude Desktop, Claude Code, Cursor, your own program. The server is a small process you write that exposes data or actions. They talk over JSON-RPC 2.0; nothing else.
What can the server expose? Three primitives, no more: Resources (read-only data identified by URIs), Tools (executable functions with schemas), and Prompts (parameterized templates). Every MCP feature you'll ever see is one of those three.
The well-run restaurant
BEFORE: You walk into a restaurant with no menu, no waitstaff, and the kitchen exposed. Want food? Walk in, learn the chef's workflow, the ingredient inventory, the prep tools — every restaurant works completely differently.
PAIN: Every visit is a new fight to discover what's possible. Switch restaurants and start from zero. Your code grows brittle adapters for every "kitchen" it ever talked to.
MAPPING: A real restaurant gives you a menu, a waiter (the protocol), and three sections: ingredients you can browse (Resources), dishes you can order that get prepared (Tools), and the chef's recommended pairings (Prompts). MCP is that menu — standardized across every kitchen your AI ever visits.
Lifecycle in three phases
Every MCP connection follows the same shape, regardless of language or transport.
- Initialize (handshake)Client sends
initializewith its protocol version. Server replies with what it supports (which primitives, which features). Client confirms withinitialized. Capabilities are now negotiated. - DiscoverClient asks "what's on the menu?" via
tools/list,resources/list,prompts/list. The server answers with names, descriptions, and JSON Schemas. No custom SDK needed. - OperateClient makes calls:
tools/call,resources/read,prompts/get. Server returns results. Repeat indefinitely until either side closes the connection. - ShutdownEither party can end the session cleanly. For stdio, that's typically the host killing its subprocess on exit. For HTTP+SSE, the client closes its event stream.
The same three phases run whether your server is a 50-line Python script or a multi-tenant cloud service.
Message flow on the wire
Trim the boilerplate and a full MCP session is just this exchange:
# 1. HANDSHAKE client → server: initialize { protocol_version: "2024-11-05", client_info: { name: "Claude Desktop" } } server → client: { capabilities: { tools: {}, resources: {}, prompts: {} }} client → server: initialized # confirmed # 2. DISCOVER client → server: tools/list server → client: [ {name, description, schema}, ... ] # 3. OPERATE (repeats) client → server: tools/call { name: "read_file", arguments: { path: "app.py" } } server → client: { content: [...] } # 4. SHUTDOWN client closes transport → server exits.
Notice the model is nowhere on this diagram. The host turns model-emitted tool_use blocks into MCP calls behind the scenes.
Misconceptions
tool_use content; the host translates that to tools/call on the right server, runs it, and returns the result as a tool_result in the next turn. The host is the bridge — the model never opens a socket.Architecture in one line: two roles (client/host + server), three primitives (Resources, Tools, Prompts), one wire format (JSON-RPC 2.0).
The lifecycle — initialize → discover → operate → shutdown — is identical for a 50-line script or a cloud service. Master that shape and every MCP server you read or write becomes obvious.
Declare three things, then run
An MCP server is small. In Python's FastMCP or the official Node SDK, the entire program is: import the library, declare any tools, declare any resources, declare any prompts, call run(). The library handles the JSON-RPC parsing, schema generation, and transport loop for you.
That means a useful filesystem server is roughly 50 lines, not the 150 you'd need for an equivalent REST service (HTTP routing, auth, CORS, OpenAPI). And the same 50 lines work with every MCP-compatible host on the planet — no per-client adapter code.
The food truck
BEFORE: Imagine wanting to share your cooking. The traditional path: lease a building, hire staff, build a kitchen, pass inspections, open a brick-and-mortar restaurant. Most great recipes never made it to the public because the overhead was crushing.
PAIN: Building a custom REST API to expose your data to AI feels the same. HTTP server, auth flow, serialization, OpenAPI spec, then a different SDK adapter for every model that wants to use it. Months of plumbing before anyone tastes your dish.
MAPPING: An MCP server is a food truck. Define your menu (tools), stock the ingredients (resources), open the service window. Any customer who speaks the protocol can order — no reservations, no per-customer adapters. Park it anywhere, serve everywhere.
The four chunks of any server
- Imports + safety boundaryBring in the SDK, then define what's in-bounds: an allowlist of file paths, a read-only DB role, a scoped API token. This is the most important code in the file — the AI can only do what your boundary permits.
- Tool definitionsDecorate each function with
@mcp.tool()(or the SDK equivalent). The decorator auto-generates the JSON Schema from your type hints, sopath: strbecomes{"type":"string"}in the wire schema automatically. - Resources + promptsDecorate read-only data getters with
@mcp.resource("uri://...")and reusable templates with@mcp.prompt(). These are NOT tools — the host treats them differently (no confirmation prompt for resources, dropdown menu for prompts). - RunCall
mcp.run(). The library reads stdin, parses JSON-RPC, dispatches to your decorated functions, writes responses to stdout. That single call is the entire transport layer.
Skip the safety boundary at your peril — an MCP server runs with the privileges of whoever launched it.
Server skeleton
# Bootstrap the server server = NEW MCPServer(name="filesystem") SET ALLOWED_ROOT = env("ALLOWED_PATHS") # 1. TOOL: action with side effects DEFINE TOOL "write_file": inputs: path, content ON_CALL(input): IF NOT is_inside(input.path, ALLOWED_ROOT): RETURN error("path out of bounds") fs.write(input.path, input.content) RETURN { ok: true, bytes: len(input.content) } # 2. RESOURCE: read-only data, addressed by URI DEFINE RESOURCE "file-tree://cwd": ON_READ(): RETURN walk(ALLOWED_ROOT, depth_limit=3) # 3. PROMPT: reusable template DEFINE PROMPT "file_summary": args: path template: "Summarize {path}: 1) one-liner 2) deps 3) issues" # 4. Listen on stdio (host launches us) server.run(transport="stdio")
Three primitives, one boundary check, one run() — that's a complete, deployable server.
Misconceptions
path: str, limit: int) and add a docstring — the SDK fills in inputSchema automatically. Hand-written schemas are only needed for unusual constraints.~/.ssh/id_rsa or any file the OS will let you open. Always validate inputs against an explicit allowlist.Building a server is 90% declaration, 10% safety. The SDK handles transport, schema, and dispatch; you write small functions and decorate them.
Always anchor the file with a safety boundary — an allowlist for paths, a read-only role for the DB, a scoped token for the API. The convenience of MCP is exactly why a boundary check is non-negotiable.
One config entry, one launched subprocess
To attach a server to Claude Desktop or Claude Code you don't write code — you write JSON. A single entry in claude_desktop_config.json (or .mcp.json for Claude Code) names a command, its arguments, and any environment variables. The host handles everything else.
On startup the host reads the config, spawns each server as a subprocess, runs the MCP handshake, and merges every server's tools, resources, and prompts into one unified menu the model sees. You can attach as many servers as you want; from the model's perspective they look like one big toolbox.
Pairing a Bluetooth device
BEFORE: Remember setting up a printer in 2002? You hunted for the right driver CD, ran an installer, restarted, and prayed the OS found the device. Every new model meant another driver, another reboot, another fight.
PAIN: Connecting AI models to tools used to feel exactly like that — per-tool SDKs, per-vendor auth flows, glue code that broke whenever a tool released a new version. Adding a fifth integration meant integrating with four other people's quirks.
MAPPING: Wiring an MCP server is like pairing Bluetooth: tell the host where to find the server (one config entry), they handshake automatically (capability discovery), and it Just Works. No drivers, no per-client SDK, no glue layer.
What happens when the host starts
- Read the configHost reads
claude_desktop_config.json(Claude Desktop) or.mcp.json(Claude Code). Each entry has a command, args, and env vars. - Spawn subprocessesFor every entry, the host runs the command (e.g.
python my_server.py) as a child process and pipes stdin/stdout. Each server is fully isolated — if one crashes, the others keep working. - Handshake each oneHost sends
initializeon each subprocess, gets capabilities back, sendsinitialized. This happens in parallel across servers. - Discover and mergeHost calls
tools/list,resources/list,prompts/liston every server and merges results into a single menu prefixed with the server name (sofilesystem.read_filedoesn't collide withgithub.read_file). - Run foreverThe model now sees the merged toolbox. Tool calls get routed back to the right subprocess. When the host quits, every server gets terminated.
Client config — what you actually write
# claude_desktop_config.json (or .mcp.json) { "mcpServers": { "filesystem": { "command": "python", "args": ["-m", "my_fs_server"], "env": { "ALLOWED_PATHS": "/Users/me/projects" } }, "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_TOKEN": "${GH_TOKEN}" } # ↑ ALWAYS use env-var expansion, never inline secrets } } }
That's the entire integration code. Add an entry, restart Claude Desktop, the new server's tools appear in the menu.
Misconceptions
.mcp.json typically gets committed to git, and even claude_desktop_config.json ends up in backups. Use ${ENV_VAR} expansion exclusively, and put real values in your shell environment or a secret manager.Connecting a client to a server is a JSON edit, not a code change. One entry per server: command, args, env. The host handles spawning, handshake, discovery, and merging.
Two non-negotiables: secrets via ${ENV_VAR} expansion, and the smallest set of servers that does the job.
Resources, Tools, Prompts — pick the right one
The choice between Resource, Tool, and Prompt isn't cosmetic — it's a safety and cost decision. Resources are read-only, browsable, and don't trigger user-confirmation prompts. Tools have side effects and usually do. Prompts are server-authored templates that show up as one-click workflows in the host's UI.
Misclassify a read-only operation as a Tool and your users get a confirmation dialog every time the AI looks at a schema. Misclassify a write as a Resource and the safety guardrails vanish. The primitive shapes the user experience.
The library, the services desk, the recommended-reading list
BEFORE: Walk into a large library that has no signs, no catalog, no staff. Books are shelved randomly. You can't tell what's reference-only, what you can check out, or what requires a librarian's help.
PAIN: Without categories, AI models face the same problem with your data. They can't tell what's safe to read, what they're allowed to modify, and what workflow to follow. Everything looks like an undifferentiated blob of "stuff I might call."
MAPPING: MCP gives you three labeled shelves. Resources are the reference section — browse and read freely, no side effects. Tools are the services desk — submit a request, get a result, the world changes. Prompts are the librarian's recommended-reading lists — pre-curated workflows the user can launch with one click.
How each one shows up to the user
- Resource → URL barThe host shows it like a file. The user (or model) can browse the list, click a URI, and see the contents. No confirmation, no token cost beyond the response payload.
- Tool → function callThe model emits
tool_use; the host typically prompts for confirmation (especially on writes), then dispatches. Each call costs ~200 framing tokens for the schema and result. - Prompt → slash command / dropdownThe host surfaces the prompt as a one-click action. User picks "Code Review", fills in the parameter, and the server returns a pre-structured message array that gets injected into the conversation.
- Wire signal: the method nameThe verb tells you the primitive:
resources/readvstools/callvsprompts/get. If you see one of those in a log, you instantly know what's happening.
Resources vs. Tools vs. Prompts
| Resource | Tool | Prompt | |
|---|---|---|---|
| Side effects? | Never | Yes | None (returns text) |
| Identified by | URI | Name + schema | Name + args |
| Wire method | resources/read | tools/call | prompts/get |
| Returns | Raw content | Action result | Message array |
| Confirmation? | No | Usually yes | Triggered by user |
| Best fit | File, schema, doc | Query, write, deploy | Code review template |
| Token overhead | Just the payload | ~200 framing tokens | Pre-built messages |
Rule of thumb: no side effects → Resource. State changes → Tool. Reusable workflow → Prompt.
Misconceptions
Pick the primitive that matches the operation's nature, not its perceived "power." Resources for reads, Tools for state-changing actions, Prompts for reusable workflows.
Misclassification is one of the cheapest bugs to ship and one of the most expensive to fix — it bakes UX friction and token cost into every session.
Production reaches for canonical servers
The toy filesystem server is great for learning the protocol, but real teams reach for a small, well-known set: GitHub (PRs, issues, code), Postgres (read-only queries), Slack (channels + alerts), Jira (tickets + sprints), and one or two custom internal-API servers. Composing these turns Claude into a genuine teammate inside your engineering loop.
Two production disciplines matter most: least-privilege (read-only by default, write only when explicitly needed) and knowing when NOT to use MCP (skills are usually the better answer for static knowledge or workflow patterns).
Hiring contractors for your house
BEFORE: You can technically hire any contractor with a hammer to do anything in your house — rewire it, repaint it, knock down a wall. They'll show up willing.
PAIN: Without scoped permissions, one over-eager contractor can demolish a wall you just wanted patched. The blast radius of a single misunderstanding is your entire kitchen.
MAPPING: Production MCP works the same way. Each server is a contractor; least-privilege is the contract. Postgres MCP gets a read-only DSN, GitHub MCP gets a scoped PAT, Slack MCP sees only the channels you allowlist. The model can't break what you didn't authorize the server to touch.
Production discipline patterns
- Read-only by defaultFor 9 out of 10 tasks, the agent needs to read state, not change it. Configure two Postgres entries: one read-only for exploration, one read-write gated behind explicit opt-in. Only enable the write server when you actually need it.
- Scope at the server, not the promptAllowlist Slack channels, Jira projects, GitHub orgs at the MCP env level (e.g.
JIRA_PROJECT_KEYS=ENG,INFRA). Don't rely on the prompt to convince the model not to call wrong scopes — restrict at the credential. - Secrets via env vars onlyEvery credential comes from
${ENV_VAR}expansion, never inline in.mcp.json. Pair with secret-manager loading at host startup so the values never touch your repo. - Audit before installingstdio servers run with full local privilege. Read the source of any community MCP server before launching it as a subprocess on your laptop. Treat them like installing an unsigned binary.
- Pair with PreToolUse hooksFor sensitive servers, add a
PreToolUsehook (Module 26) that double-checks the tool name and parameters before execution — cheap defense-in-depth.
Skill, MCP, or both?
# Common architectural question: # do I write a Skill or an MCP server? IF capability is workflow / domain knowledge: # "here is how we deploy to k8s" USE a Skill # markdown is auditable; team can read it ELIF capability needs LIVE data or actions: # "query current state of prod orders" USE an MCP server # skills can't fetch / write live state ELIF capability is reusable command: # "/run-migration" USE slash command + Skill # slash = entry point; skill = procedure ELIF needs both knowledge AND live state: # "resolve a deploy incident" USE Skill that calls an MCP server # skill describes; MCP acts ELSE: DEFAULT to Skill # auditable beats opaque when in doubt
Heuristic: if the answer is "I want Claude to know something", write a skill. If it's "I want Claude to do something against live state", build an MCP server.
Misconceptions
Production MCP is 80% configuration discipline, 20% server code. Read-only by default, secrets via env vars, scopes at the credential, audit before install, hooks for defense-in-depth.
And remember the architectural fork: skills for knowledge, MCP for live state, both together when the workflow needs both.
One question per concept
Tap a card to reveal the answer.
initialize → server responds with capabilities → client sends initialized to confirm. Only after that final confirmation does either side make any real tool, resource, or prompt calls. The handshake establishes the protocol version and the menu of supported features.def read_file(path: str) with @mcp.tool() and the SDK auto-generates {"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}. The docstring becomes the description. Hand-written schemas are only needed for unusual constraints.Open the full module on desktop
The desktop version walks through building a real MCP server with FastMCP, registering all three primitives, wiring it into Claude Desktop via claude_desktop_config.json, comparing stdio vs HTTP+SSE in code, and shipping a production-ready .mcp.json with GitHub, Postgres, Slack, and Jira servers.
Module 7 of 30 · Track 2: Tool Use