MCP: A Cross-System Standard for Tool Integration
Before MCP existed, every (host, tool) pair needed its own bespoke integration. If you wanted Claude to read Postgres, somebody had to write the glue. If you also wanted Cursor to read the same Postgres, that was a second copy of the glue. A home-grown agent reaching in was a third. The wiring did not compose.
Model Context Protocol is Anthropic's answer to that mess, open-sourced in November 2024 and now the de-facto standard for how LLM applications talk to tools. By April 2026 it is everywhere: Claude Code speaks it natively, Cursor exposes MCP servers as a first-class feature, OpenAI's Responses API and Agents SDK accept MCP servers as a tool surface, and there is a public registry of servers you can point any MCP-speaking host at. The interesting thing about MCP is that the protocol itself is not novel. It's deliberately small; that's most of why it works.
N × M integrations
If you've built anything with tool-using models, you've felt this. There are some number of hosts you want to support: Claude Code, Cursor, a custom agent you wrote, maybe an internal copilot. Call that number . There are also some number of tools you want them all to reach: a filesystem, a GitHub client, a database, a Slack client, whatever. Call that .
If each (host, tool) pair needs its own custom adapter, you write adapters. Worse, every one of them is bespoke: different authentication, different argument conventions, different result formats. Adding a new host means rewriting every integration. Adding a new tool means rewriting it in every host. This is the integration-hell pattern the industry has lived through more than once (USB, ODBC, LSP). The fix is always the same: standardize a protocol in the middle, and the cost drops to adapters. One client per host, one server per tool.
That is MCP. The hero animation above is the entire thesis of the protocol, playing on a loop. Three hosts on the left, four servers on the right, and a clean collapse from custom wires to standard adapters once an MCP bus sits between them.
Watch the two numbers diverge as you scale. The widget below walks through a plausible catalogue, one host or tool at a time, and ticks up versus alongside it. Bespoke grows like area; MCP grows like perimeter. By the time you're at six hosts and eight tools you're looking at 48 custom integrations vs 14 adapters, and the ratio only gets worse from there.
- ○Claude Code
- ○Cursor
- ○Custom agent
- ○OpenAI Responses
- ○Internal copilot
- ○CI bot
- ○Filesystem
- ○GitHub
- ○Postgres
- ○Slack
- ○Sentry
- ○Jira
- ○S3
- ○Stripe
Each step adds a host on the left or a tool on the right. The red bar is the bespoke count (); the green bar is the MCP count (). Watch the gap open up once both sides have more than a couple of members.
This post is the foundational MCP intro: what the protocol looks like locally, what a server exposes, how a host talks to it, and why it won. The follow-up post, Remote MCP, OAuth, and Enterprise Auth, picks up the HTTP-transport and auth story. I'll skip past that here and just signpost where the split lives.
Prereqs
This one sits late in the series. It assumes you already have:
- Function calling as structured generation from the function-calling post. You know why
tool_useblocks are JSON that match a schema, and you know how the host validates them. - The agent loop from the-agent-loop. You know the
model → runtime → tool → resumecycle, and that a tool "call" isn't really the model running code. The model emits a request and the host executes it. - Transcript formats from transcript-formats. You know that different providers represent tool use differently in their transcripts.
MCP doesn't replace any of those. It sits one layer up, standardizing how the host finds tools and calls them, while the provider-specific transcript format is still whatever it was. I'll flag the seam when we get there.
The minimal formalism
MCP is plain JSON-RPC 2.0 over a transport. If you have never seen JSON-RPC, the shape is almost trivial: a request has an id, a method name, and params; a response has the same id and either a result or an error.
No framing format to learn, no schema language to adopt. Everything MCP does is a small, fixed set of method names on top of that envelope:
initialize— hello, what protocol version do you speak, what capabilities do you offer?tools/list,tools/call— discover and invoke tools.resources/list,resources/read— discover and read resources.prompts/list,prompts/get— discover and fetch prompt templates.notifications/…— one-way messages, e.g. "the tool list just changed."
The method names are the interesting part. They separate the protocol (how the client and server talk) from the implementation (what a given tool actually does). I think this is the durable intuition from the whole topic. A file-reading tool and a database-querying tool share nothing in common at the implementation level, but from the model's point of view they both answer tools/list with a schema and tools/call with a result. That's what makes cross-system reuse possible at all.
There are two transports, and this is the one place I'll signpost the next post:
- stdio for local processes. The host launches the server as a subprocess and talks to it over stdin/stdout. This is the simple, 90%-of-what-you'll-see case, and it's what we'll use here.
- streamable HTTP for remote servers. Long-lived connections, server-sent events, OAuth 2.1 with PKCE for authentication. Everything about remote MCP (the HTTP framing, the auth dance, the registry story) is the whole subject of the next post. Local first.
The lifecycle, step by step
Here is the whole handshake. A host boots, discovers what a server can do, watches the model decide to use a tool, forwards the call, and hands the result back to the model. Eight steps, on autoplay.
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": { "protocolVersion": "2025-06-18" }
}The MCP lifecycle: initialize to agree on a protocol version and capabilities, tools/list to discover what the server exposes, then (once the model emits a tool use) tools/call to execute it. Everything is plain JSON-RPC; the transport just carries the envelopes.
A few things worth pulling out of the animation.
The initialize exchange is the one place you have to be careful about versions. The client proposes a protocol version; the server either accepts or counter-proposes. Capabilities are declared up front, so a host knows before it asks whether a server exposes resources, prompts, sampling, or just tools. Once that handshake is done, everything else is steady-state discovery and dispatch.
The tools/list call is what makes MCP composable. The server publishes every tool's schema in the same JSON-schema shape that the model has already been trained to read, and the host takes those schemas and hands them straight to the model as its tool list for this turn, with no translation layer in between.
And the key insight: the model never talks to the MCP server directly. The model emits a tool_use block into its transcript (exactly as in the function-calling post); the host reads that block and issues a tools/call; the server does its work and returns a result; the host injects that result back into the transcript as a tool_result; the model resumes. MCP is entirely between the host and the server. The provider-specific transcript format (Anthropic content blocks, OpenAI tool_calls, Harmony channels) stays whatever it was. This is the protocol-versus-implementation split applied twice: once between client and server, once between host and model.
Three primitives, not one
It's tempting to summarize MCP as "tools," but the protocol has three primitives, and the other two are doing real work in the ecosystem. The panel below is the concrete version.
{
"name": "github_create_issue",
"description": "Open a new GitHub issue in a repo.",
"inputSchema": {
"type": "object",
"properties": {
"repo": { "type": "string" },
"title": { "type": "string" },
"body": { "type": "string" }
},
"required": ["repo", "title"]
}
}MCP's three primitives: tools (the model decides to call them), resources (the host attaches them as context), and prompts (the user picks them from a menu). Different control surfaces, different consumers, one protocol.
The split matters because it maps to who is in control. Tools are for the model, which looks at the schemas and decides to invoke one. Resources are for the host and the user; a code editor might attach git://repo/HEAD to every turn, or a user might drop a file into the chat by URI. Prompts are for the user, showing up as slash commands, menus, or saved workflows that the user explicitly invokes. Collapsing all three into "MCP is tools" loses the most interesting design choice in the spec.
In practice, resources are the primitive that tends to be under-used. A database-backed MCP server that exposes a live postgres://orders/last_24h resource is a very different object than one that exposes a run_query tool. The tool version lets the model write SQL; the resource version hands the model a document the host has already fetched. Which one you want depends on how much you trust the model with your database, and those are different questions.
Worked example
Let's make it concrete. I'm going to build the smallest useful MCP server I can think of (a calculator exposing add and multiply) and walk through what happens when a host connects to it.
The server comes up. It registers two tools with JSON schemas. A host (imagine Claude Code) launches it as a subprocess over stdio and runs initialize. The server responds with its capabilities: { tools: {} }, meaning "I expose tools." The host calls tools/list, gets back the two schemas, and hands them to the model on the next turn.
Now the user types "what is 7 times 9?" The model sees two tools with their schemas. It picks multiply and emits a tool_use with {a: 7, b: 9}. The host sees the tool_use, maps it back to multiply, and issues a JSON-RPC tools/call request over stdin to the calculator subprocess. The subprocess does the arithmetic, sends back {content: [{type: "text", text: "63"}]} over stdout, and the host injects that result into the next model turn. The model reads "63" and replies to the user.
Everything in that trace was a JSON blob on a pipe. No framework, no provider-specific handshake. The protocol is deliberately simple, and that simplicity is why it works. If I swap Claude Code for Cursor, the server doesn't change. If I swap the calculator for a GitHub client, the host doesn't change. The wiring composes.
Implementation sketch
Here is the whole server in the official Python SDK. Thirty-five lines, including imports and a decorator per tool.
# pip install mcp
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("calc")
@mcp.tool()
def add(a: float, b: float) -> float:
"""Add two numbers."""
return a + b
@mcp.tool()
def multiply(a: float, b: float) -> float:
"""Multiply two numbers."""
return a * b
if __name__ == "__main__":
# Serve over stdio. The host launches this as a subprocess and
# talks to us on stdin/stdout using JSON-RPC.
mcp.run(transport="stdio")# And a tiny client, also using the SDK. In practice the host (Claude Code,
# Cursor, your app) is doing this for you, but it helps to see it.
import asyncio
from mcp.client.stdio import stdio_client, StdioServerParameters
from mcp import ClientSession
async def main():
params = StdioServerParameters(command="python", args=["calc_server.py"])
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print([t.name for t in tools.tools]) # ['add', 'multiply']
out = await session.call_tool("multiply", {"a": 7, "b": 9})
print(out.content[0].text) # '63'
asyncio.run(main())The @mcp.tool() decorator is doing exactly one piece of work: it introspects the Python function's type hints and docstring and publishes them as a JSON schema in the server's tools/list response. Everything else is the SDK taking care of the JSON-RPC envelope. When the host issues tools/call with {"name": "multiply", "arguments": {"a": 7, "b": 9}}, the SDK validates the arguments against the schema, invokes the Python function, wraps the return value as {"content": [{"type": "text", "text": "63"}]}, and sends it back.
The numbers in the client snippet match the handshake animation above. Seven times nine is still sixty-three.
Two things worth noticing. First, the server doesn't know or care what model is on the other end. It publishes a schema; whoever is talking to it can use it. Second, the client doesn't know or care what language the server is written in. The TypeScript SDK would have produced a subprocess this Python client could talk to just as happily. This is the protocol/implementation split again, doing real work.
Misconceptions
"MCP is just function calling with extra steps." It's close enough to feel right, but it misses where the value actually is. Function calling is a contract between one host and one model on one turn: here is a schema, here is a call, here is a result. MCP is a contract between any host and any tool provider, ahead of time, over many turns and many tools. It also adds resources and prompts, which function calling has no equivalent for. If you only ever use one model and one toolset, yes, you don't need MCP. The moment you have two of either, the problem shows up and MCP starts paying for itself.
"MCP is Anthropic-only." No. It's an open spec under the modelcontextprotocol GitHub org with reference implementations in Python, TypeScript, Java, Kotlin, C#, and more. OpenAI supports it in the Responses API and the Agents SDK, Cursor treats MCP servers as first-class citizens, and there are community clients for every major editor and agent framework by now. Anthropic started it, the community adopted it, and the ecosystem is bigger than any one vendor. The closest historical parallel is the Language Server Protocol, which Microsoft started for VS Code and which every other editor ended up speaking because the economics were obvious.
"You need an LLM to use MCP." Also no, and this one is more interesting than it sounds. An MCP client is anything that speaks the protocol. Plain scripts can invoke MCP servers as a unified interface to local tools without any model involved: call the filesystem server's read_file tool, pipe it into a different server's analyze_log tool, wire together something like a Unix pipeline over JSON-RPC. The protocol doesn't care who is driving. It just cares that someone is making valid requests.
"MCP is a framework." It isn't. It's a spec, and a set of SDKs that make the spec easy to implement. You can write an MCP server with no SDK at all if you want, because the wire format is documented and it's JSON. The SDKs give you decorators and transport plumbing. The protocol is the actual commitment.
What's next
This post covered local MCP: why the problem exists, what the protocol looks like as JSON-RPC methods on a stdio pipe, the three primitives (tools, resources, prompts), and the smallest possible calculator server. The intuition to walk away with is the protocol/implementation split. MCP standardizes how clients and servers talk, not what any particular tool does, and that is the thing that makes cross-system reuse possible.
The next post covers Remote MCP, OAuth, and Enterprise Auth: what changes when the server is an HTTP endpoint instead of a subprocess, how streamable HTTP and server-sent events carry the same JSON-RPC traffic, and why OAuth 2.1 with PKCE ended up as the canonical auth story for MCP in the enterprise.
Additional reading (and watching)
-
Anthropic. (2024). Introducing the Model Context Protocol. The original announcement post, November 2024, laying out the motivation and the initial open-source release.
-
Model Context Protocol. (2025–2026). Specification. The canonical specification of the protocol, covering lifecycle, primitives, and transports. Versioned; the April 2026 revision is 2025-06-18.
-
JSON-RPC Working Group. (2013). JSON-RPC 2.0 Specification. The underlying RPC envelope MCP uses. Small, stable, and the right choice for a protocol that wants to be implementable in a weekend.
-
Model Context Protocol. Python SDK. The
mcppackage on PyPI. TheFastMCPhelper used in the worked example is the high-level path; the low-level server API is also in the same repo. -
Model Context Protocol. TypeScript SDK. The other reference SDK. Together with the Python one these set the shape most third-party implementations follow.
-
OpenAI. (2025). Agents SDK: MCP support. Documentation of how the OpenAI Agents SDK consumes MCP servers as a tool source; MCP support also landed in the Responses API in 2025.
-
Microsoft. (2016–). Language Server Protocol. The canonical prior art: same argument for editors and language analyzers. LSP is the design template MCP is openly copying.
-
Model Context Protocol. Server Registry. Public directory of MCP servers, including Anthropic-maintained reference implementations (Filesystem, GitHub, Postgres, Slack, Sentry) and community contributions.