What is MCP?
The Model Context Protocol explained from first principles — why it exists, how its three-layer architecture works, and what the JSON-RPC 2.0 wire format looks like under the hood.
- ✓Explain the "integration hell" MCP solves and why a shared protocol beats per-app plugins
- ✓Describe the Host → Client → Server layered architecture and each layer's responsibilities
- ✓Read a JSON-RPC 2.0 tool-call round-trip and identify every field
- ✓Choose the right primitive (Tool / Resource / Prompt) for a given use case
- ✓Compare stdio vs HTTP/SSE transport and explain when each is appropriate
The Problem MCP Solves
Imagine every USB device required its own custom port cut into your laptop chassis. Your keyboard needs a 6-pin oval port, your mouse needs a trapezoidal one, your monitor needs something proprietary, and every new device requires buying a new laptop.
That was the state of AI app integrations before MCP. Every IDE, every chat interface, and every AI coding assistant invented its own plugin API, its own authentication handshake, and its own schema for describing what tools were available. Teams at GitHub spent months building a GitHub Copilot integration that worked exclusively in VS Code. Notion built their own Claude integration. Salesforce built a separate Einstein integration. None of these could share server code, configuration, or tooling.
MCP is the USB-C of AI integrations. One open protocol, and any host that speaks it can connect to any server that speaks it — the same way USB-C lets your phone charge from your laptop, your monitor, or a hotel lobby kiosk.
Before MCP, the pattern was always the same:
- An AI app (Claude Desktop, Cursor, a custom agent) wants to access some external capability.
- The app team writes a bespoke adapter — usually REST HTTP calls with hand-rolled JSON schemas.
- The server team writes a bespoke counterpart that only works for that app.
- When a second app wants the same capability, the server team writes a second adapter.
- Six months later, there are 5 adapters, they all slightly disagree on schema formats, and updating the underlying service breaks all of them.
The GitHub MCP server launched in March 2025. Within 48 hours, it worked with Claude Desktop, Cursor, Windsurf, and any agent built on the Anthropic SDK — with zero changes to the server. That's the payoff: build the server once, expose it everywhere. Real teams report 70–90% reduction in integration maintenance overhead after moving to MCP.
MCP was proposed by Anthropic in November 2024 and immediately adopted by major IDEs (Cursor, Windsurf, Zed) and cloud vendors (Cloudflare, Supabase, MongoDB). The spec is open, the reference implementations are MIT-licensed, and the protocol is LLM-agnostic — a Claude Desktop plugin works equally well as an OpenAI tool server.
The MCP Architecture
MCP defines three distinct roles. These are not the same as "client" and "server" in the REST sense — the naming is more specific:
Notice the layering: the Host and Server never communicate directly. Every message is brokered by the Client. This is intentional — it means the host can connect to dozens of servers without writing transport code for each one, and the server never needs to know which host it's talking to.
In practice, the Client lives inside the same process as the Host (it's typically a library import, not a separate binary). The Server is almost always a separate process — either a local subprocess (stdio) or a remote service (HTTP/SSE).
JSON-RPC 2.0 Wire Format
jsonrpc version field, a numeric id, a method string, and a params object. Responses mirror the same id and carry either a result or an error field. No HTTP required — works over any transport. is a transport-agnostic RPC protocol. MCP uses it for every message — initialization, capability negotiation, tool calls, resource reads, and prompt fetches. Each message is a JSON object terminated by a newline (\n).
Here is a complete tool-call round-trip. Every field shown here is part of the official MCP spec:
Step 1 — Client sends a tool_call request
{
"jsonrpc": "2.0",
"id": 42,
"method": "tools/call",
"params": {
"name": "search_documents",
"arguments": {
"query": "MCP transport options",
"limit": 5
}
}
}
Step 2 — Server sends the result
{
"jsonrpc": "2.0",
"id": 42,
"result": {
"content": [
{
"type": "text",
"text": "Found 3 results for 'MCP transport options':\n1. stdio: local subprocess, lowest latency\n2. HTTP/SSE: remote service, sharable across clients\n3. WebSocket: bidirectional streaming (experimental)"
}
],
"isError": false
}
}
Step 3 — If something went wrong
{
"jsonrpc": "2.0",
"id": 42,
"error": {
"code": -32602,
"message": "Invalid params: 'limit' must be between 1 and 100"
}
}
The id field is how the client matches a response to a request — both sides must mirror it. The method is always a namespaced string like tools/call, resources/read, or prompts/get. When errors occur, they come back in the error field (never in result), and the standard JSON-RPC error codes (-32700 to -32603) apply.
You will almost never write these JSON messages by hand — the SDK handles all serialization. But seeing the wire format demystifies what the SDK is doing, and it's essential for debugging with tools like mcp dev (MCP Inspector).
The Three Primitives
Every MCP server exposes its capabilities using exactly three building blocks. Choosing the right one determines how the LLM interacts with your data or logic.
Tools (Actions)
A Tool is a function the LLM can invoke that may have side effects. The LLM describes what it wants to do, the host routes it through the client to the server, and the server executes it. Use tools for: API calls, database writes, sending messages, running computations, triggering workflows.
Resources (Data)
A Resource is a URI-addressed piece of data the LLM can read into its context. Resources are always read-only — they have no side effects. They can be static (a file) or dynamic (a live database row). Use resources for: files, database records, API snapshots, configuration, documentation. Think of them as "managed context injection."
Prompts (Templates)
A Prompt is a reusable prompt template stored on the server. The host can ask the server to list its prompts, then inject one into the conversation. This moves prompt engineering to the server side, where it can be versioned, tested, and deployed without touching client code. Use prompts for: standardized analysis patterns, domain-specific instructions, workflow openers.
| Primitive | Side effects? | Who initiates? | Typical use |
|---|---|---|---|
| Tool | Yes (or pure compute) | LLM decides | write_file(), query_db(), send_slack() |
| Resource | No — read only | LLM or user | file://report.pdf, db://order/1234 |
| Prompt | No | User / host UI | "Review this PR", "Summarize ticket" |
Two Transports
MCP is transport-agnostic at the protocol level, but the spec defines two official transports. Your choice between them determines where your server can run and who can connect to it.
| Property | stdio (local) | HTTP + SSE (remote) |
|---|---|---|
| How it works | Server is a subprocess; host writes to its stdin, reads its stdout | Server is an HTTP service; host POSTs requests, server streams responses via Server-Sent Events |
| Best for | Local tools: file system, shell, local DB, dev tools | Shared services: APIs, cloud DBs, team-wide resources |
| Latency | Lowest (same machine, no network) | Network-dependent |
| Multi-client | No — one host spawns one subprocess | Yes — multiple hosts can connect to one server |
| Auth | OS-level (process ownership) | OAuth 2.0 (required for public servers) |
| Deployment | Local binary or script | Any HTTP host: VPS, Cloud Run, Lambda, Fly.io |
| Streaming | Newline-delimited JSON on stdout | SSE: data: events on the event stream |
Imagine you're building a customer service hotline for a bank. If you need one agent who handles calls only for the internal dev team on one floor, you give them a desk phone and an extension — fast, simple, not accessible from outside. That's stdio.
If you need the same agent to handle calls from any branch office, any ATM, and the mobile app simultaneously, you deploy a proper call center with a published number, authentication, and queuing. That's HTTP/SSE.
The underlying agent (your server code) is identical in both scenarios — only the telephony infrastructure changes. MCP works the same way: switch from mcp.run() to mcp.run(transport="sse") in FastMCP, and the server suddenly accepts HTTP connections without changing a single tool function.
Capability Negotiation Handshake
Before the LLM can call any tool, the client and server must complete a structured handshake. This is how the host discovers what a server can do — and it happens automatically every time a connection is established.
The capabilities object in the server's initialize response tells the client which primitives the server supports — tools, resources, prompts, and optional extensions like sampling and logging. If the server doesn't advertise a capability, the client won't request it.
{
"protocolVersion": "2024-11-05",
"serverInfo": {
"name": "my-document-server",
"version": "1.0.0"
},
"capabilities": {
"tools": { "listChanged": true },
"resources": { "subscribe": false, "listChanged": false },
"prompts": {}
}
}
The listChanged field tells the client whether the server can notify it when the tool list changes at runtime (useful for servers that load plugins dynamically). subscribe on resources means the client can ask to be notified when a specific resource URI changes.
The handshake is fully automatic when you use the MCP SDK — your tool functions are registered with the framework, which builds the capabilities object and handles all initialize/list messages before your code even runs. You only write the tool implementations.
Real-World Deployments
| Product | Role | Transport | Notes |
|---|---|---|---|
| Claude Desktop | Host (+ client manager) | stdio | Reads claude_desktop_config.json; spawns each server as a subprocess |
| Cursor | Host (+ client manager) | stdio / HTTP | Supports both; project-level .cursor/mcp.json config |
| Windsurf | Host | stdio | Early MCP adopter; global + workspace config |
| VS Code (Copilot Chat) | Host | stdio / SSE | MCP support added in VS Code 1.96 (Dec 2024) |
| GitHub Copilot | Host | HTTP/SSE | Remote servers; auth via GitHub OAuth |
| Cloudflare Workers AI | Server deployment platform | HTTP/SSE | Deploy MCP servers to the edge with Workers |
| Supabase | Server | HTTP/SSE | Official Supabase MCP server (DB + auth tools) |
| GitHub | Server | HTTP/SSE | Search repos, read files, create issues/PRs as tools |
| Claude Code | Host | stdio | Config via .claude/settings.json mcpServers |
The GitHub MCP server is a production example worth studying. In a single server it exposes ~30 tools (search_repositories, create_pull_request, get_file_contents, list_issues, etc.) and ~20 resources (repository files, issue bodies, PR diffs). Any host that speaks MCP can use all of them — that's one server codebase serving Claude Desktop, Cursor, Windsurf, VS Code, and any custom agent simultaneously.
The pattern across all these deployments is the same: configure the host with the server's location (command + args for stdio, or URL for HTTP/SSE), and the capability negotiation handshake takes care of the rest. The host discovers the tools automatically — there's no manual "register this tool" step on the host side.
Knowledge Check
Five questions to verify you've got the protocol concepts solid before building your first server in MCP01.
1. In the MCP architecture, which component manages the connection to a specific server and handles capability negotiation?
2. What is the correct JSON-RPC 2.0 method name for calling a tool?
tool.invokecall_tooltools/callmcp.tools.execute3. You want to expose the contents of a large SQL database table so the LLM can reference it as background context — but you don't want the LLM to write to it. Which MCP primitive is most appropriate?
4. Your team wants to share one MCP server across 12 developers and also deploy it to a CI/CD pipeline. Which transport should you use?
--multi-client flag5. During the capability negotiation handshake, what does the listChanged: true field inside the tools capability object tell the client?
notifications/tools/list_changed notification when its tool list changes at runtime