Before MCP, every agent integration was custom code — one wrapper for OpenAI, another for Anthropic, another for LangChain. After MCP, you write the connection once and any agent can use it — Claude Desktop, ChatGPT, Cursor, VS Code, your homegrown loop. Anthropic released it in November 2024 and within 18 months it became the USB-C of AI tooling.
Scope. This lesson teaches the protocol and agent architecture — the three primitives (Tools/Resources/Prompts), the JSON-RPC handshake, transports, and how to build an MCP server that any agent can consume. For the Claude Code CLI workflow — how to register MCP servers in .claude/settings.json, the /mcp slash command, alwaysLoad, auth patterns, and per-server recipes — see MCP: External Connections (in Claude Code).
Learning Objectives
After this lesson, you will be able to:
Explain why MCP became the industry standard for connecting LLMs to tools, data, and prompts — Anthropic's open protocol replacing fragmented per-vendor tool APIs
Distinguish MCP's three primitives — Tools, Resources, Prompts — and when each is the right abstraction for your integration
Write a basic MCP server in Python or TypeScript that exposes a custom tool to any MCP-compatible client (Claude, ChatGPT, Cursor, VS Code)
Pick the right MCP transport (stdio for local, HTTP/SSE for remote) and integrate the official MCP SDK into your agent stack
Why MCP became the "USB-C of AI" within 18 months of launch -- Anthropic shipped MCP in November 2024; by mid-2026, Claude, ChatGPT, Gemini, Cursor, VS Code, JetBrains, and Replit all support it as the standard tool-integration protocol
Why you stop writing per-LLM tool integrations once you adopt MCP -- before MCP, every tool needed an OpenAI tools wrapper + Anthropic tools wrapper + LangChain wrapper; MCP gives you ONE server that works with all of them
Why MCP servers became the new "API-first" play for SaaS companies in 2025-2026 -- GitHub, Slack, Notion, Linear, Stripe, Postgres all ship official MCP servers; building an MCP server is the new "have an API"
Build this --> Write a 50-line Python MCP server that exposes one tool ("query my SQLite database") and connect it to Claude Desktop or VS Code; watch the LLM use it transparently with no per-vendor wrapper code
MCP has three architectural roles. Conflating them is the #1 source of confusion when you start writing your first server.
Role
What it is
Examples
Host
The application the user actually interacts with. Embeds an LLM and one or more MCP clients.
Claude Desktop, Cursor, VS Code Copilot, Zed, a custom LangChain app.
Client
A library inside the host that speaks the MCP protocol on behalf of one server. The host typically runs one client per connected server.
The mcp Python SDK's ClientSession; the TS SDK's Client class.
Server
The thing that exposes Tools / Resources / Prompts. Owned by whoever built the integration (GitHub, Postgres, your team).
mcp-server-github, mcp-server-postgres, your sqlite_mcp.py.
The host:client:server fan-out is 1:N:N — one host instantiates N clients to talk to N servers. Each client is paired with exactly one server. The host arbitrates which tools from which servers get exposed to the LLM, and routes the LLM's tool calls back to the right client.
A useful mental model: the host is the OS, the client is the device driver, and the server is the peripheral. The host doesn't know how a printer works; the printer driver does. Same here — the host doesn't know how to query Postgres; the Postgres MCP server does, and the matching client in the host translates protocol messages.
text
+-------------------+ +------------+ +------------------+
| Host | === | Client A | <==> | MCP Server A |
| (Claude Desktop | +------------+ | (filesystem) |
| or Cursor) | +------------+ +------------------+
| | === | Client B | <==> | MCP Server B |
| embeds 1 LLM | +------------+ | (github) |
| + N clients | +------------+ +------------------+
| | === | Client C | <==> | MCP Server C |
+-------------------+ +------------+ | (your custom DB) |
+------------------+
Trace a request through the Host, Client, and Server roles to see how the 1:N:N fan-out routes messages.
MCP defines three things a server can expose. They look superficially similar — all three send JSON over the wire — but each maps to a different interaction pattern.
1. Tools — functions the LLM can call autonomously. Same shape as OpenAI/Anthropic function calling, just standardized. The LLM decides when to invoke them based on its prompt. Examples: query_users, send_slack_message, run_terminal_command. Pattern: model-initiated action.
2. Resources — data the LLM (or user) can read on demand. Identified by URIs like file:///path/to/doc.md, postgres://table/users, or github://repo/owner/branch. Resources are typically attached to a conversation by the user (drag-and-drop, file picker, slash command) rather than fetched autonomously by the model — though servers can implement either pattern. Pattern: data made available to context.
3. Prompts — pre-built prompt templates the user can explicitly invoke via slash commands or menus. They take typed arguments (e.g., /summarize_pr {pr_number}) and expand into a full templated message that's sent to the LLM. Pattern: user-initiated workflow.
The cleanest mental separation:
Primitive
Who triggers it
Typical UI surface
Tool
The model, mid-conversation
(invisible) — tool calls appear inline
Resource
The user (attach), then model reads
File picker, "@" mention
Prompt
The user explicitly
Slash command, menu item
What Do You Think?
You're building an MCP server for your company's analytics warehouse. The LLM should be able to run SQL queries autonomously, but you also want users to drag in a saved dashboard's data as context. How do you expose these?
When a client connects to a server, both sides exchange a capabilities object during the initialize step. This is how each side declares what it supports — and equally important, what it does NOT support. The handshake prevents protocol-version drift between clients and servers that may have shipped months apart.
The interesting flags are the nested ones: tools.listChanged: true means the server can push notifications when its tool list changes (e.g., a tool was just enabled by an admin). resources.subscribe: true means clients can subscribe to live updates of a resource. If a side doesn't advertise the capability, the other side must not invoke the corresponding operation — the JSON-RPC call will return a "method not supported" error.
The protocol version field is exact-match in spirit but spec-defined for graceful negotiation: if client and server disagree, the lowest mutually-supported version wins, and unsupported methods simply error. This is what keeps the ecosystem from fragmenting.
MCP servers run on the user's machine (stdio) or behind OAuth (HTTP+SSE), and the host is responsible for enforcing the user's trust boundary. The model itself is treated as untrusted — it can call any advertised tool — so the security perimeter sits between the model and the tool execution.
Three practical layers in 2026:
Process isolation. stdio servers run as separate subprocesses launched by the host. The host can restrict the subprocess's working directory, environment variables, and (on Linux) seccomp/landlock policies. The filesystem server, for instance, ships with an allowlist of paths it will read or write.
Per-tool permission prompts. Claude Desktop, Cursor, and VS Code intercept the model's tool calls and pop a UI confirmation for any "write" or "irreversible" action the first time it appears in a session ("Allow this server to write to ~/Documents/notes.md?"). The host caches the user's decision per server + tool + scope.
OAuth for remote servers. HTTP+SSE servers exposing SaaS data (Linear, Stripe, Notion) require an OAuth flow per user. The token lives in the host's secure storage. The server's authorization scope determines which tools the LLM can actually invoke — even if the protocol advertises delete_account, the user's token may only have read:issues.
The trust model in one line: the LLM is treated as a potentially-confused user; the host is the gatekeeper; the server is the system that gets called. Permission prompts exist because the host has to translate an LLM's intent into an actual filesystem or network operation, and the user is the only entity authorized to grant that translation.
Quick check
A user installs an MCP server that exposes a `delete_file` tool. The LLM (without any user intent) decides to call it on `~/Documents/important.md`. What SHOULD happen?
Most early MCP servers use stdio for simplicity. Remote SaaS-grade MCP servers (Stripe, Linear, Notion) use HTTP+SSE with OAuth.
Concretely, the two transports map to two operational models:
Concern
stdio
HTTP + SSE
Process model
One subprocess per host launch; dies when host exits
Long-running HTTP service; survives host restarts
Auth
None at protocol level — relies on local filesystem trust
OAuth 2.1 with PKCE for first-party; bearer token for service-to-service
Latency overhead
~1ms (pipe round-trip)
5-50ms (HTTP round-trip + TLS)
Multi-tenant
No — one user per process
Yes — single deployment serves many users
Best fit
filesystem, sqlite, local git, terminal
GitHub, Linear, Stripe, internal SaaS
The split is sharp in practice: if the server needs to read the user's local files, stdio is the only sane choice (you can't HTTP into a laptop). If the server is a hosted SaaS, HTTP+SSE with OAuth is the only sane choice. There's no real middle ground.
When a client calls tools/list, the server replies with a JSON array of tool descriptors. Each descriptor follows a strict shape — name, description, and a JSON Schema for arguments. Let's parse one in Python and validate it, exactly the way a client SDK would internally.
Loading visualization...
What you just wrote IS, in miniature, an MCP client. The real mcp SDK has more validation (full JSON Schema, async transport, error mapping), but the protocol-level operations are exactly: parse the tool list, validate arguments, dispatch by name. Try adding a new tool to TOOLS_LIST_RESPONSE and confirming the dispatcher rejects unknown names — that's the same defense-in-depth a real client uses.
You want to expose a custom 'query_company_wiki' tool to Claude AND Cursor AND your custom LangChain agent. Best approach?
The answer: one MCP server, three hosts. You write the tool logic once. Each host (Claude Desktop, Cursor, your LangChain agent via the MCP client SDK) connects to the same MCP server. Any LLM the host runs (Claude / GPT-4 / Gemini) can use it through the client's translation layer.
Use MCP when
Stay with native tool API when
You're integrating multi-host or multi-LLM
Single-LLM single-host (just use OpenAI tools or Anthropic tools natively)