Part
6
  |  
MCP and the Agent Frontier
  |  
Chapter
19

The Model Context Protocol

Every AI integration you've built is a one-off. MCP is the reason your next hundred won't be.
Reading Time
12
mins
BACK TO CLAUDE MASTERCLASS

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.

The Fragmentation Problem

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:

  1. Nothing was reusable. A tool built for one model host couldn't be ported to another without rewriting the transport layer, the schema, and often the business logic.
  2. Debugging was archaeological. When something broke — and it always broke — you had to trace through custom JSON formats, bespoke error codes, and ad-hoc retry logic that varied per integration.
  3. The ecosystem couldn't grow. If every tool requires a custom connector for every host, the number of integrations you need scales as tools times hosts. That's quadratic complexity, and it kills adoption.
Framework · The Quadratic Connector Problem · QCP

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.

The Architecture: Clients, Servers, and the Host

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:

  1. The client establishes a connection to the server and negotiates capabilities. The server declares what it can do — which tools are available, which resources are exposed, what schemas they expect.
  2. The client sends structured requests. Not raw API calls, not free-form text — structured, typed JSON-RPC messages.
  3. The server validates the request, executes the operation, and returns a structured response.
  4. The client aggregates results from multiple servers into a unified context for the model.
JSON-RPC under the hood

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.

Building Your First MCP Server

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.

Common first mistake

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.

Tools vs. Resources: The Distinction That Matters

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.

✕ Without this distinction
  • Every capability is a function call
  • No separation between reading and acting
  • Model can't browse available data without side effects
  • Schema becomes a mess of optional parameters
✓ With tools and resources separated
  • Actions are explicit and auditable
  • Data access is read-only and safe to explore
  • Model can discover available information before deciding to act
  • Each capability has a clear, focused schema

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.

Schema Validation: Trust but Verify

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.

Key takeaway

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.

Why This Changes the Economics of AI Integration

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.

What to Do Monday Morning

Install the MCP framework and build a working server

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.

Add Pydantic validation to every resource

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.

Map your current integrations to the Quadratic Connector Problem

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.

Connect your MCP server to Claude Desktop

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.