The Concept
MCP — Model Context Protocol — is the specification that lets LLM clients talk to external tools and data sources through a single, uniform interface. It's the layer Claude Desktop, Cursor, and a growing list of agent runtimes use to load capabilities at runtime instead of compiling them in. Every tutorial I've read about it leads with "AI integration standard," which is true and useless.
Here's the framing that actually transfers: MCP is what an SDK becomes when the consumer is a language model. If you've ever shipped a client library — or even consumed one carefully — you already understand most of MCP. The remaining 20% is the part where the caller can't read your docs.
If You Already Know SDKs, You Already Know Most of This
What does an SDK actually do? It wraps a backend with a stable, typed interface so callers don't have to think about transport, serialization, auth, or versioning. The Stripe SDK doesn't add capability — Stripe's HTTP API has the same capability. The SDK adds a contract: here are the methods, here are their signatures, here's how errors come back, here's how we'll evolve this without breaking you.
MCP does the same thing for tool and data access, with the wire shifted one layer up. An MCP server exposes a set of named tools, each with a JSON Schema for its inputs and a structured response shape for its outputs. A client connects, asks "what do you have?", gets the schemas back, and can now invoke any of them. That's an SDK. The only differences are that the contract is published over the wire instead of compiled into a library, and the consumer is an LLM client deciding at runtime which method to call.
The mapping:
| SDK | MCP |
|---|---|
| Methods on a client object | tools exposed by a server |
| Read-only accessors / data fetchers | resources (URI-addressable read endpoints) |
| Prebuilt helpers / templates | prompts (named, parameterized prompt templates) |
| Type signatures | JSON Schema on each tool's inputSchema |
| Constructor / config | initialize handshake (protocol version + capabilities) |
| SDK version | MCP protocol version, negotiated per session |
| Library installed in your app | Server process the client launches or connects to |
| Errors thrown by the client | JSON-RPC error responses |
The reason this analogy holds isn't aesthetic. It's that MCP solves the same problem an SDK solves: it decouples a capability provider from its consumers. Build the server once, every MCP-aware client can consume it. That's the entire point of an SDK — write once, integrate many.
What's Actually New
There are three real differences. They matter.
The caller can't read your docs. When a programmer uses an SDK, they read the README and pick the right method. When an LLM uses an MCP server, it picks methods from the description field returned by list_tools. The description is the documentation, and it's read by a model that has never seen your code. This means the description is part of the contract, not commentary on it. A clear, narrow description with explicit constraints is the difference between a tool that gets called correctly and one that gets called constantly with bad arguments.
Discovery is at runtime, not compile time. You don't import an MCP server. The client connects, sends tools/list, and gets back whatever the server decides to expose right now. Servers can change what they expose mid-session. This makes MCP closer to LSP or ODBC than to a traditional SDK — the contract is negotiated, not linked. Practical consequence: a single LLM app can pick up new capabilities without redeploying, just by adding a server to its config.
The transport is part of the spec. SDKs assume HTTP or gRPC and wrap it. MCP defines two transports — stdio (for local subprocesses) and HTTP+SSE (for remote servers) — and standardizes the JSON-RPC 2.0 framing on both. You don't get to invent your own. This is the dull-but-load-bearing kind of standardization, the same kind that made USB-C useful: any compliant client can talk to any compliant server, and nobody has to write glue.
Under the Hood
A minimal MCP server in TypeScript, exposing one tool:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
const server = new Server(
{ name: "runbook-server", version: "0.1.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "search_runbooks",
description:
"Full-text search over team runbooks. Returns up to 5 matching " +
"paragraphs with their source path. Use for operational questions " +
"about deploys, incidents, on-call procedures.",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Search query" },
limit: { type: "integer", minimum: 1, maximum: 20, default: 5 },
},
required: ["query"],
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (req) => {
if (req.params.name !== "search_runbooks") {
throw new Error(`Unknown tool: ${req.params.name}`);
}
const { query, limit = 5 } = req.params.arguments as {
query: string;
limit?: number;
};
const hits = await searchRunbooks(query, limit); // your code
return {
content: [{ type: "text", text: formatHits(hits) }],
};
});
await server.connect(new StdioServerTransport());
That's the whole shape. ListToolsRequestSchema is your schema export. CallToolRequestSchema is your method dispatcher. The transport is one line. Everything else is the body of your function — exactly the code you'd write inside an SDK method.
The handshake is worth understanding once because it surfaces in every debugging session:
- Client opens the transport (spawns your process, or opens an SSE connection).
- Client sends
initializewith its protocol version and capabilities. - Server responds with its protocol version and capabilities.
- Client sends
notifications/initialized. - Normal request/response begins.
tools/list,tools/call,resources/read, etc.
If a server appears in a client's config but never shows up as available, the failure is almost always at step 2 or 3 — version mismatch, server crashed before responding, or stderr noise polluting the stdio framing. (Stdio transport reserves stdout for JSON-RPC; logging to stdout corrupts the stream. Always log to stderr.)
Resources and prompts follow the same pattern: list_* returns metadata, get_* / read_* returns content. If you understand tools/list + tools/call, you understand all three primitives.
Decision Framework
Use MCP when:
- You're building tool-using behavior across more than one LLM client. If a capability needs to work in Claude Desktop and a custom agent and an IDE plugin, build it as an MCP server once instead of three integrations.
- The client app isn't yours. If you're shipping capability to Claude Desktop or Cursor or anything else you don't control, MCP is the only contract those hosts speak. You don't get to invent a different one.
- You want runtime-pluggable capabilities. Add a server, restart the client, the new tools appear. No build step.
Don't use MCP when:
- You're building a single agent with a fixed toolbelt. Just register the tools directly with whatever LLM SDK you're already using. The MCP indirection costs you a process boundary and a JSON-RPC round-trip per call. For a closed system, it's overhead with no payoff.
- Latency budget is tight. stdio MCP adds milliseconds; remote MCP adds tens to hundreds. If you're inside a tight inner loop (per-token tool routing, real-time UI), call the function directly.
- The "tool" is a one-line prompt fragment. Don't build a server for something that belongs in the system prompt.
What Your Manager Thinks It Does vs. What It Actually Does
Your manager thinks MCP is an "AI integration framework" or "the USB-C of AI."
What it actually is: a JSON-RPC contract for exposing tools, resources, and prompts to LLM clients. It's the SDK pattern lifted to the wire so that the contract is language-agnostic and discoverable at runtime. The reason it caught on isn't the protocol design — JSON-RPC is thirty years old. It's that it gave hosts (Claude Desktop, IDEs, agent runtimes) a single integration point and gave capability providers a single distribution surface. That's a network-effect play, not a technical breakthrough.
The reframe for your next meeting: "MCP is the LSP of AI tools. Same shape — host process, capability servers, JSON-RPC over stdio, runtime discovery. Anyone who's used a language server in VS Code already knows the architecture."
Ship This Weekend
Build an MCP server that exposes your team's runbooks (or any folder of markdown) as a search_runbooks tool, and connect it to Claude Desktop.
-
npm init -y && npm install @modelcontextprotocol/sdk -
Drop the server above into
server.ts. ReplacesearchRunbookswith a real implementation —ripgrepshelled out, or a tiny in-memory index overglob("**/*.md"). Keep it under 100 lines. -
Add the server to Claude Desktop's MCP config (
~/Library/Application Support/Claude/claude_desktop_config.jsonon macOS):{ "mcpServers": { "runbooks": { "command": "node", "args": ["/absolute/path/to/server.js"] } } } -
Restart Claude Desktop. Ask: "What's our deploy rollback procedure?" Watch it call
search_runbooksand answer from your actual docs.
Two things you'll learn. First, the description field on your tool matters more than the function body — try shipping it once with a vague description and once with a precise one and watch the calling behavior change. Second, the moment you have one MCP server running, adding the second one is trivial — and that's the actual value proposition. Capability composition stops being an integration project and becomes a config edit.
Further Reading
- Model Context Protocol Specification (modelcontextprotocol.io) — The actual spec. Short and readable. Skim the architecture overview, then jump to the message types you care about.
- MCP TypeScript SDK (GitHub) — Reference implementation. The examples directory is the fastest path to a working server.
- Language Server Protocol Specification (microsoft.github.io) — The architectural ancestor. Reading the LSP spec after the MCP spec makes the design choices behind MCP obvious.
- JSON-RPC 2.0 Specification (jsonrpc.org) — Two pages. If you've never read it directly, do — it's the actual wire format under MCP and demystifies a lot of the framing.