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.
Here is the math that drawer represents. 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. It does not get better as the numbers grow. It gets quadratically worse.
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 is not that. It is a wire format and a handshake, the boring layer underneath the frameworks. It says: here is how a thing that has tools announces them, here is how a thing that wants tools asks, here is the 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 did not tell Dell how to build a laptop. It said any laptop and any charger that both speak this standard will work, and the moment enough of them did, the adapter drawer stopped growing. Protocols win by being uninteresting. The interesting part is what they let other people build without asking you first.
Here’s the part nobody tells you about standards: the value is not in the spec, it is in the second implementation. One MCP server is a demo. The hundredth server, 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
The thing that sold me was how small a real server is. 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 not a nicety. It 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 did not integrate with any of them. I implemented the handshake once and they showed up.
The honest part: it is early, and thin
I would be lying if I told you the ecosystem is there yet. 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.
The transport story is the part that 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. It is a reason to know what you are signing up for.
The bet
So why am I building on it anyway, this early, with the road half-paved? Because I am not betting on any one client, and I am not betting on the current crop of servers either. I am betting on the protocol.
Bet on a framework and you are exposed to that framework’s roadmap, its funding, its maintainers losing interest. Bet on a client and you are married to one company’s app. Bet on the protocol and you are exposed to something different: 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 do not win because the early version is good. The early version is never good. 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. I will take the thin, awkward, weeks-old standard over another adapter, and I will bet on the second implementation showing up. That is the whole bet. What’s in your drawer?