I have a drawer at home full of charging cables. Micro-USB, the old wide Apple one, two kinds of barrel jack, a Mini-USB for a camera I no longer own. Every device shipped its own connector, and every connector needed its own adapter, and the combinatorics of that drawer is exactly the combinatorics of every AI integration I built before late last year.
That drawer is a math problem. You have N tools you want a model to use: a database, a ticketing system, a code repo, an internal search index. You have M things doing the calling: your chat app, your agent runtime, someone’s IDE plugin, a batch job. The naive world wires each caller to each tool by hand, and you are now on the hook for N times M integrations, every one with its own auth quirks, its own JSON shape, its own way of saying “that argument was wrong.” I have lived inside that multiplication, and it gets quadratically worse the moment either number grows.
The Model Context Protocol, which Anthropic put out in late November, collapses that multiplication. You build a server once per tool, you build a client once per app, and any client speaks to any server. N times M becomes N plus M. That is the entire pitch, and if you have ever maintained an integration layer you already feel why it matters more than the seventh agent framework released this month.
Why this isn’t just another framework
Frameworks are opinions about how you should write your code. They live in your process, they own your control flow, and switching one out means a rewrite. MCP works a level below that, as a wire format and a handshake, the boring layer underneath the frameworks. It defines how a thing that has tools announces them, how a thing that wants tools asks, and what envelope a tool call and its result travel in. It does not care what model you run or what framework wraps it.
That distinction is the whole reason I trust it more than the frameworks it sits beneath. USB-C never told Dell how to build a laptop; it only promised that any laptop and any charger that both spoke the standard would work, and the moment enough of them did, the adapter drawer stopped growing. Protocols win by being uninteresting, and the payoff is everything other people build on top of them without asking you first.
A standard earns its keep in the second implementation, not the spec. One MCP server is a demo; the hundredth, written by people who have never met each other against the same handshake, is an ecosystem. The spec is just the contract that lets strangers build the other half of your system.
What a server actually looks like
What sold me was how small a real server turns out to be. I wrote one in an afternoon to expose an internal incident database to whatever client wanted it. No framework, no orchestration layer, just a process that announces a couple of tools and answers when they are called. Stripped down, the shape is this.
# A tiny MCP server over stdio. Two tools, nothing clever.
# The whole point: the model never touches our DB. It asks; we decide.
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
app = Server("incident-db")
@app.list_tools()
async def list_tools() -> list[Tool]:
# This is the announcement. Any client reads this and now knows
# what we can do, without us having pre-agreed on anything.
return [
Tool(
name="search_incidents",
description="Find past incidents by free-text query. Read-only.",
inputSchema={
"type": "object",
"properties": {
"query": {"type": "string"},
"limit": {"type": "integer", "default": 5},
},
"required": ["query"],
},
),
Tool(
name="incident_timeline",
description="Return the event timeline for one incident id.",
inputSchema={
"type": "object",
"properties": {"incident_id": {"type": "string"}},
"required": ["incident_id"],
},
),
]
@app.call_tool()
async def call_tool(name: str, args: dict) -> list[TextContent]:
# The server owns the boundary. We validate, we scope, we audit.
# I do NOT hand the model a connection string and hope.
if name == "search_incidents":
rows = db.search(args["query"], limit=args.get("limit", 5))
return [TextContent(type="text", text=render(rows))]
if name == "incident_timeline":
rows = db.timeline(args["incident_id"]) # parameterized, always
return [TextContent(type="text", text=render(rows))]
raise ValueError(f"no such tool: {name}") # fail loud, not weird
async def main():
async with stdio_server() as (read, write):
await app.run(read, write, app.create_initialization_options())
Read past the syntax and notice what the structure is doing. list_tools is the announcement: the server tells any client what it can do, in a schema the client did not have to know in advance. call_tool is the boundary: the model never gets a database handle, it gets to ask, and the server decides whether to answer and how. I can put authorization, rate limits, and an audit log right there in one function, on the safe side of the line. The model proposes. The server disposes. For anything that touches money or customer data, that split is the only design I would put my name on.
The same server, unchanged, works against a desktop chat client, against an agent I wrote myself, against a colleague’s experiment I have never seen. I never integrated with any of them; I implemented the handshake once and they showed up.
Early, and thin
I would be lying if I told you the ecosystem is there yet, because it is not. This thing is weeks old in public. The reference servers are mostly toys, the auth story for anything beyond a local process is still being argued over in GitHub issues, and most of the clients that matter to you probably do not speak it yet. If you go looking for a mature directory of production-grade servers today, you will be disappointed, and anyone telling you otherwise is selling something.
Transport is what will bite people first. The clean case is a local server over stdio, which is genuinely a few lines. The moment you want a remote server with real authentication and multiple tenants, you are out past where the paved road ends, writing the hard parts yourself and watching the spec move under you. (I have already rewritten one server’s auth twice, and I expect a third.) None of this is a reason to wait, only a reason to know what you are signing up for.
The bet
Why build on it this early, with the road half-paved? Not for any single client, and not for the servers that exist today. The bet is on the protocol.
Bet on a framework and you are exposed to its roadmap, its funding, its maintainers losing interest. Bet on a client and you are married to one company’s app. The protocol is a different kind of exposure: the chance that a critical mass of people independently decide the handshake is worth implementing, and after that the work compounds in your favor instead of against you. I think that critical mass is coming, because the alternative is everyone keeping their own drawer of adapters, and nobody who has maintained that drawer wants to keep doing it.
Standards never win on the merits of their early versions, which are always rough. They win when enough people are tired enough of the multiplication to agree on one connector and move on. I have been tired of N times M for years, so I will take the thin, awkward, weeks-old standard over another adapter and bet on the second implementation showing up. That is the whole bet.