Most engineers hear "protocol" and think of something academic — an RFC gathering dust in a standards body while the real world ships whatever compiles. So when Anthropic released the Model Context Protocol, the default reaction was polite indifference. Another abstraction layer. Another interface nobody asked for. The industry already had function calling, tool-use APIs, and a dozen wrapper libraries. Why would anyone adopt a protocol when duct tape was working fine?
The duct tape was not working fine. It was holding, the way duct tape always holds — until the third API changes its authentication scheme, the fourth tool returns a shape your parser doesn't expect, and the fifth integration needs to run in a different environment from the first four. Every AI integration I've seen in production shares the same structural problem: the connection between the model and the tool was written as a one-off, and it aged like one.
Every AI integration I've seen in production shares the same structural problem: the connection between the model and the tool was written as a one-off, and it aged like one.
MCP exists because the AI industry's approach to integrations was the software equivalent of building a new electrical plug for every appliance. Every model talked to every tool differently. Nothing was portable. Nothing was discoverable. And every time you moved to a new model or a new host application, you rewired from scratch.
Before MCP, the landscape looked roughly like this: you had an LLM that needed to call a function, read a file, query a database, or hit an API. Each of those capabilities required a custom integration. The model's tool-calling interface was one format. The API's authentication was another. The response shapes were whatever the developer felt like returning that day. Error handling was "try/except and hope."
This created three compounding failures:
When every tool needs a custom integration for every host application, the number of connectors scales as tools multiplied by hosts. MCP collapses this to tools plus hosts — each tool implements the protocol once, and each host connects to it through the same interface.
This is not a theoretical concern. I've seen teams maintaining fifteen separate API wrapper scripts, each with its own authentication pattern, its own error format, and its own maintainer who left the company six months ago. MCP doesn't make integrations trivial. It makes them standard, which is far more valuable.
MCP is a client-server protocol, but the terminology is precise and worth getting right early.
The host is the application process that runs the AI model — Claude Desktop, an IDE plugin, a custom agent runtime. Inside the host, one or more MCP clients manage connections to external capability providers. Each capability provider is an MCP server: a lightweight process that exposes tools, resources, or both through the protocol.
The message flow is always the same:
MCP uses JSON-RPC 2.0 as its wire format. If you've worked with LSP (the Language Server Protocol that powers IDE features), the architecture will feel familiar — and that's intentional. MCP borrows the same client-server-host pattern that made LSP successful.
The critical insight is what the model doesn't do. The model never talks to tools directly. It never authenticates with an API. It never parses a raw HTTP response. Every external interaction is mediated through MCP, which means the model can focus on reasoning and decision-making while the protocol handles access, execution, and safety.
┌─────────────────────────────────────────┐
│ Host Application │
│ ┌───────────┐ ┌───────────┐ │
│ │ MCP Client│ │ MCP Client│ │
│ └─────┬─────┘ └─────┬─────┘ │
│ │ │ │
└────────┼───────────────┼────────────────┘
│ │
MCP Protocol MCP Protocol
│ │
┌─────┴─────┐ ┌─────┴─────┐
│ MCP Server │ │ MCP Server │
│ (Local) │ │ (Remote) │
│ Files, DB │ │ APIs, SaaS │
└────────────┘ └────────────┘
The model never talks to tools directly. Every external interaction is mediated through MCP — the model reasons, the protocol handles access.
Enough architecture diagrams. Here is a working MCP server in Python using the FastMCP framework, which handles the protocol plumbing — message formatting, JSON-RPC handling, schema generation, and the transport layer.
from mcp.server.fastmcp import FastMCP
# Create the server instance — this name is visible to any
# connecting client, including Claude Desktop
mcp = FastMCP("TaskAnalyzer")
@mcp.tool()
def analyze_task(description: str, priority: int) -> dict:
"""Analyze a task description and return structured metadata."""
summary = description.strip().capitalize()
if priority >= 8:
level = "critical"
elif priority >= 5:
level = "medium"
else:
level = "low"
return {
"original": description,
"summary": summary,
"priority_level": level
}
@mcp.resource("notes://list")
def list_notes() -> list[str]:
"""Return available notes."""
return [
"Review Q3 deployment plan",
"Update API documentation",
"Schedule architecture review"
]
@mcp.resource("notes://{note_id}")
def get_note(note_id: str) -> str:
"""Retrieve a specific note by ID."""
return f"Contents of note: {note_id}"
if __name__ == "__main__":
mcp.run()
Three things to notice about this code.
First, type annotations drive everything. The analyze_task function declares description: str and priority: int. FastMCP reads those annotations and auto-generates the JSON schema that clients use to validate inputs before sending them. If a client sends a string where an integer is expected, the protocol catches it before your code ever executes.
Second, the @mcp.tool() and @mcp.resource() decorators are the entire registration API. You don't write schema files, you don't configure routes, you don't build a manifest. The decorator inspects the function and handles everything.
Third, mcp.run() starts the event loop on stdio. The server listens for JSON-RPC messages from whatever client connects to it. It looks like the process is hanging — that's correct. It's waiting for a client to send a message.
New developers try to inspect the server in the same terminal that's running it. That won't work — the server is blocking on stdio. Open a second terminal, activate the same virtual environment, and run fastmcp inspect server.py to see the tools and resources your server exposes.
MCP separates capabilities into two categories, and conflating them is one of the fastest ways to build a server that confuses both clients and models.
Tools are actions. They accept inputs, execute logic, and produce outputs. A tool might analyze text, fetch weather data, create a file, or query a database. The model invokes tools — they are the verbs of your MCP server.
Resources are data. They expose read-only information through URI-based endpoints. A resource might return a list of documents, the contents of a configuration file, or metadata about the server itself. The model reads resources — they are the nouns.
Resources also support parameterized URIs. The pattern notes://{note_id} tells the client that any URI matching that shape — notes://42, notes://alpha — should be routed to the same function, with the variable segment passed as an argument. This is the same pattern used in REST APIs, but applied to a protocol designed for AI consumption rather than human browsing.
Here's a mistake I see repeatedly: developers build MCP servers that return whatever Python dictionary their function produces, without validating the output against a schema. The tool works in testing because the data is clean. In production, a missing field or a wrong type silently corrupts the model's context, and the failure surfaces three reasoning steps later as a hallucination nobody can trace.
FastMCP does not automatically enforce output schemas on resources. Resources are intentionally lightweight. If you want strict output control — and in production you always do — you validate explicitly using Pydantic.
from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, ValidationError
mcp = FastMCP("ValidatedServer")
class Task(BaseModel):
id: int
title: str
priority: str
class TaskList(BaseModel):
tasks: list[Task]
@mcp.resource("tasks://list")
def list_tasks() -> dict:
"""Return validated task list."""
data = {
"tasks": [
{"id": 1, "title": "Deploy staging", "priority": "high"},
{"id": 2, "title": "Update docs", "priority": "low"},
]
}
validated = TaskList(**data)
return validated.model_dump()
When the data matches the schema, Pydantic returns a validated object. When it doesn't — say, someone passes a string where id expects an integer — Pydantic raises a ValidationError, and MCP reports it as a resource failure instead of silently returning garbage. This is the difference between a demo server and a production server.
MCP doesn't make integrations easy — it makes them standard. One protocol, one schema format, one message flow, regardless of whether the tool reads a local file, queries a database, or calls a remote API. The value isn't in any single integration being simpler. It's in the hundredth integration costing the same as the first.
The real impact of MCP isn't technical — it's economic. Before MCP, every new tool integration was a project. You needed to understand the API, build the connector, handle authentication, design error recovery, and test the whole chain. That's a week of work per integration, minimum.
With MCP, the protocol handles transport, schema negotiation, and message formatting. The server handles capability exposure and validation. The client handles aggregation and context management. What's left for you is the business logic — the actual thing the tool does. That's a few hours of work, not a week.
This changes who can build integrations. It changes how many integrations a team can maintain. And it changes the break-even point on whether an integration is worth building at all. When the cost of connecting a new tool drops from a week to an afternoon, tools that were never worth connecting suddenly become viable.
When the cost of connecting a new tool drops from a week to an afternoon, tools that were never worth connecting suddenly become viable.
Create a virtual environment, install fastmcp, and write a server with one tool and one resource. Run it with python server.py and inspect it from a second terminal with fastmcp inspect server.py. Confirm both the tool and the resource appear in the capability summary.
Define a Pydantic model for each resource's return type. Validate outputs explicitly before returning them. Deliberately break the schema — pass a string where an integer is expected — and confirm that MCP raises a validation error instead of returning bad data.
List every tool and API your AI system currently connects to. Count how many have custom connectors. For each one, ask: could this be an MCP server that any host application could connect to? The integrations where the answer is "yes" are your migration candidates.
Configure Claude Desktop to recognize your MCP server. Send a prompt that requires the tool, and watch Claude invoke it through the protocol. This is the moment the architecture stops being theoretical.
MCP is not a framework for building demos. It's a protocol for building systems that survive contact with production — where APIs change, schemas drift, and the person who built the integration left the company six months ago.