
Hey guys, Mr. Technology here.
Let me describe a situation you've probably hit if you've shipped anything serious with an AI agent. You want Claude — or Gemini, or whatever model you're using — to have access to your codebase, your database, your Slack, your GitHub repos, and your internal APIs. All at once. On demand. You wire up the integrations, point the model at everything, and it falls over. Not because the model is bad. Because you just gave it 80 tools and a 200K-token context window, and it has no idea which tool to reach for when you ask it to "check if the checkout API is returning the right status codes."
That's the problem Anthropic's Model Context Protocol was built to solve. And in the last six months, it went from an Anthropic experiment to the closest thing the AI agent industry has to a universal standard.
Let me explain why that matters, what the protocol actually gets right, where the production implementations are still a mess, and what you should actually be doing if you're building agent systems today.
Six months ago, every LLM provider had their own tool-calling interface. OpenAI's function calling schema. Anthropic's tool definitions. Google's plugin format. If you wanted your agent to work with multiple providers — or switch between them — you were writing translation layers. Lots of them. And if you were a tool provider, you were maintaining integrations for every model separately. It was the adapter hell of the AI world.
MCP changed that by becoming the neutral wire protocol that both sides could agree on. It started as Anthropic's spec, but it was good enough and simple enough that Google adopted it for Gemini, Microsoft integrated it into Azure AI Studio, and the open-source ecosystem built dozens of reference servers. The providers didn't agree to a committee standard — they adopted one that already worked.
This is worth dwelling on, because it's the same reason USB-C won over earlier attempts at a universal connector. USB-C wasn't technically superior to every alternative at launch. It won because it was good enough, Apple adopted it, and the rest of the industry followed to avoid being the outlier. MCP is USB-C for AI tools. The analogy is uncomfortable for people who want to believe it's a carefully engineered standard, but it's accurate, and USB-C is a good standard.
Here's where I lose most people, because they hear "protocol" and assume they need to read a spec. You don't. But you need to understand the three-layer model underneath it.
The transport layer handles how your MCP client (the thing running in your agent) talks to an MCP server (the thing exposing a tool or resource). The spec supports two transports: stdio and Server-Sent Events (SSE). Stdio is what you use for local servers — it's a subprocess communication channel that works like any command-line program piping JSON in and out. SSE is HTTP-based and what you'd use for remote servers. Most production deployments use SSE behind a reverse proxy because it lets you handle auth, rate limiting, and TLS termination at the infrastructure layer.
The message layer is JSON-RPC 2.0. Every request, response, and notification is a JSON object with a jsonrpc field set to "2.0". This is intentionally boring — JSON-RPC 2.0 has been around since 2010, every language has libraries for it, and it handles batch requests cleanly. Anthropic didn't reinvent anything here.
The capability layer is where the interesting design choices live. MCP servers advertise what they can do through capability objects. The three core capabilities are:
tools — functions the LLM can callresources — data the LLM can read (like a filesystem or a database query result)prompts — templated prompts the server can generateWhen your agent connects to an MCP server, it first calls initialize and receives the server's capability manifest. Your agent runtime parses this, decides what to surface to the LLM, and handles the dispatch when the model calls a tool.
Here's what that looks like in practice with the Python MCP SDK:
from mcp.server import Server
from mcp.types import Tool, TextContent
from mcp.server.stdio import stdio_server
server = Server("my-filesystem-server")
@server.list_tools()
async def list_tools():
return [
Tool(
name="read_file",
description="Read contents of a file",
inputSchema={
"type": "object",
"properties": {
"path": {"type": "string"},
"lines": {"type": "integer", "default": 100}
},
"required": ["path"]
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "read_file":
with open(arguments["path"]) as f:
lines = arguments.get("lines", 100)
return [TextContent(type="text", text="".join(f.readlines()[:lines]))]
raise ValueError(f"Unknown tool: {name}")This is a 30-line MCP server that exposes a file reading tool. You can run it as a subprocess and connect your agent to it. The LLM sees a read_file tool with a clear schema. Your agent runtime dispatches calls to it. It works.
That example works. Here's what doesn't work when you try to scale it.
When your LLM calls a tool, it usually has a timeout baked in — 30 seconds for most API calls, 60 seconds for Claude's computer use. But MCP servers can run operations that exceed those limits. A database query might take 8 seconds. A web search might take 20. A code execution sandbox might hit 90 seconds before it times out.
If your MCP runtime doesn't handle this, the LLM call fails even though the operation was probably going to succeed. The fix is async execution with result callback, but most implementations handle this poorly or not at all. Here's what a production timeout strategy looks like in the dispatch layer:
import asyncio
from typing import Any
from functools import partial
TOOL_TIMEOUTS = {
"filesystem_read": 10,
"filesystem_write": 15,
"database_query": 30,
"web_search": 45,
"code_execution": 120,
"default": 30,
}
async def dispatch_with_timeout(server, tool_name: str, arguments: dict) -> Any:
timeout = TOOL_TIMEOUTS.get(tool_name, TOOL_TIMEOUTS["default"])
try:
result = await asyncio.wait_for(
server.call_tool(tool_name, arguments),
timeout=timeout
)
return result
except asyncio.TimeoutError:
# Log, alert, and surface a clean error to the LLM
raise ToolTimeoutError(f"Tool '{tool_name}' exceeded {timeout}s timeout")The MCP spec doesn't specify timeout behavior. The spec leaves it as an exercise for the implementer. That's fine — the spec can't know your use case. But it means every MCP runtime has to make these decisions explicitly, and most teams don't until they hit a production incident.
This is the one that bites you hardest when you're scaling. The LLM's context window is your most expensive resource. Every tool schema you send to the model consumes tokens. If you're running 50 MCP servers with 20 tools each, that's 1,000 tool schemas. Sending all of them to the LLM on every request is expensive and slow, and it degrades model performance because the model has to reason over a massive tool list.
The right architecture is a two-level system: a capability index that your MCP runtime maintains, and a routing layer that only surfaces the relevant subset of tools to the LLM based on the current task. The LLM doesn't need to know about all 1,000 tools. It needs to know about the 10 that are relevant right now.
Anthropic's skills system does exactly this — skills are MCP servers that only get loaded when they're relevant to the current task. You don't load the Kubernetes debugging skill when you're building a frontend feature. You don't load the database schema browsing skill when you're reviewing a pull request. The routing is explicit, and it respects your context window budget.
When an LLM calls a tool through MCP, you need to be able to trace it. Which tool was called, with what arguments, how long did it take, what did it return, did it error? Without this, debugging an agent in production is archaeology. You have the agent's output but no link between the agent's decision and the tool's behavior.
The standard approach is wrapping each MCP tool call in a span and propagating trace context through the call chain. Here's what that looks like using OpenTelemetry:
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
@server.call_tool()
async def call_tool(name: str, arguments: dict):
with tracer.start_as_current_span(f"mcp.{name}") as span:
span.set_attribute("mcp.tool.name", name)
span.set_attribute("mcp.tool.arguments", json.dumps(arguments))
try:
result = await actual_dispatch(name, arguments)
span.set_attribute("mcp.result.count", len(result))
span.set_status(Status(Code.OK))
return result
except Exception as e:
span.record_exception(e)
span.set_status(Status(Code.ERROR, str(e)))
raiseThis gives you traces that connect the LLM span to the tool span. You can see in your observability dashboard that Claude called database_query at 14:23:07 with this SQL, it took 4.2 seconds, and returned 47 rows. That's the minimum you need for production debugging.
MCP servers execute code with the credentials of the process running them. A compromised or malicious MCP server is a full host compromise. The MCP spec acknowledges this — it says servers should be trusted — but it doesn't give you much guidance on how to achieve that.
Here's the practical security model: your MCP client is trusted, and the MCP servers it connects to are trusted by whoever configured them. The tool definitions that servers expose to the LLM are not trusted — they're untrusted input that your runtime should validate before executing anything.
@server.call_tool()
async def call_tool(name: str, arguments: dict):
# Validate arguments against allowed patterns before touching the filesystem
if name == "filesystem_read":
path = arguments["path"]
# Block path traversal attacks
if ".." in path or path.startswith("/"):
raise ValueError("Path must be relative and within allowed directory")
real_path = SAFE_BASE / path
if not real_path.is_relative_to(SAFE_BASE):
raise ValueError("Access denied: path outside sandbox")
# Validate database queries for injection
if name == "database_query":
sql = arguments["query"]
if not is_select_only(sql):
raise ValueError("Only SELECT queries are allowed")
return await actual_dispatch(name, arguments)The MCP ecosystem doesn't have a certification program or a security audit standard. You're relying on the community to vet servers, and most of the servers in the wild haven't been audited by anyone. If you're running third-party MCP servers, you should be running them in isolated processes or containers with minimal privilege.
I've looked at a lot of production MCP deployments in the last six months. Here's what the median serious deployment looks like:
5 to 20 MCP servers connected to a single agent runtime. Most teams start with 3 or 4 (filesystem, GitHub, Slack, and a database server) and add more as they find use cases. I've seen one team running 47 MCP servers for a complex DevOps agent that touched every internal system.
The runtime is usually Node.js or Python. The MCP SDKs in both languages are mature and well-maintained. TypeScript is more common for server implementations; Python is more common for agent runtimes that integrate with data science or ML infrastructure.
The transport is stdio for local servers, SSE for remote servers. Most teams run MCP servers as local subprocesses for development and testing, then expose them via SSE behind a reverse proxy for production. The reverse proxy handles TLS, auth, and rate limiting.
The observability is an afterthought. Most teams add OpenTelemetry tracing after their first major incident. The ones who add it from day one are the ones who learned the hard way on a previous project.
The security is minimal. Most MCP servers run as the same user as the agent runtime. There are no process-level sandboxing, no capability-based security, and no audit logging. This is the most alarming aspect of the current state of the ecosystem and the one that's most likely to cause a high-profile incident.
Here's the thing that irritates me most about the current MCP ecosystem: there's no Docker Hub.
npm has 2 million packages. PyPI has 500,000. Docker Hub has 100,000 containers. The MCP ecosystem has a handful of reference servers maintained by Anthropic and a scattered collection of community servers with varying quality, no security auditing, and no discoverability infrastructure.
If you want a Postgres MCP server, you have a few options. If you want a Datadog MCP server, you have one option. If you want a Figma MCP server, you're probably writing it yourself. The long tail of integrations is missing because there's no platform economics for MCP servers yet — nobody's making money from niche MCP servers, so nobody's building them to production quality.
The closest thing to a registry is the MCP Hub at mcp-hub.com, which aggregates community servers, and Anthropic's own servers GitHub repo. Neither has security scanning, version tracking, or quality ratings. The commercial platforms — Azure AI Studio, Google Vertex AI Agent Builder — each have their own MCP server collections that are only accessible within their ecosystems.
This is the gap I expect to close in the next 12 months. Someone — probably a well-funded startup, possibly Anthropic itself — will build a public MCP registry with security scanning, version management, and quality ratings. When that exists, the long tail of tool integrations becomes a solved problem and the MCP ecosystem matures from "interesting experiment" to "production-grade infrastructure."
MCP won because it solved a coordination problem. LLM providers needed a standard way to expose tools. Tool providers needed a standard way to reach models. Neither side wanted to build and maintain a dozen bilateral integrations. MCP became the shared vocabulary.
The real question isn't whether MCP wins. It's whether the next generation of tool calling — whatever that looks like — makes MCP obsolete in 18 months. I think the answer is: probably not. Tool calling is going to remain a core part of agentic AI for the foreseeable future, and the cost of switching protocols is high enough that providers will resist change unless something dramatically better comes along.
What you should do today: if you're building AI agents in 2026 and you're not using MCP, you're building integration work that won't survive the next provider switch. If you're building MCP infrastructure and you're not thinking about timeout handling, observability, and security from day one, you're building systems that will fail in production in ways that are expensive to fix.
The teams that get this right are the ones treating MCP as infrastructure, not as a feature. They're building MCP runtimes with proper timeout budgets, full OpenTelemetry tracing, and security models that assume the servers they run are untrusted. They're contributing to the ecosystem because they know a rising MCP tide lifts all agent boats.
MCP is not a solved problem. It's a protocol with a good foundation and a lot of production engineering still to be done. If you're the kind of engineer who likes building the infrastructure others depend on, this is one of the most interesting spaces in AI right now.