Model Context Protocol

If you’ve built AI integrations before you know the shape of the problem. Connecting Claude to Slack requires custom code. Switch to GPT? Rebuild it. Add GitHub? Write another integration. Multiply this across every tool and every AI provider, and you’re drowning in glue code.

MCP (Model Context Protocol) fixes this. Released by Anthropic in November 2024 and now governed by the Linux Foundation with backing from OpenAI, Google, and Microsoft, MCP has become the industry standard for connecting AI to external systems. Anthropic’s own framing is USB-C for AI, one protocol that lets any compliant client talk to any compliant server. The analogy is doing real work, and it’s also the sort of claim a protocol makes about itself on day one.

The short version: you build one MCP server and any compliant client can use it. The protocol is JSON-RPC 2.0, it has three primitives, and there are SDKs for Python and TypeScript among others.

The Problem MCP Solves

Before MCP, connecting an AI assistant to external tools meant the classic Mร—N integration problem. Five AI providers times ten tools equals fifty custom integrations. Each one with its own authentication, its own error handling, and its own maintenance burden.

MCP inverts that. Each tool exposes one server, each AI application implements one client, and the protocol handles what’s in between:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                       MCP HOST                          โ”‚
โ”‚  (Claude Desktop, Cursor, VS Code, custom apps)         โ”‚
โ”‚                                                         โ”‚
โ”‚    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚    โ”‚ MCP Client  โ”‚  โ”‚ MCP Client  โ”‚  โ”‚ MCP Client  โ”‚   โ”‚
โ”‚    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
            โ”‚                โ”‚                โ”‚
      JSON-RPC 2.0     JSON-RPC 2.0     JSON-RPC 2.0
            โ”‚                โ”‚                โ”‚
     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”
     โ”‚ MCP Server  โ”‚  โ”‚ MCP Server  โ”‚  โ”‚ MCP Server  โ”‚
     โ”‚  (GitHub)   โ”‚  โ”‚  (Postgres) โ”‚  โ”‚   (Slack)   โ”‚
     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Core Architecture

MCP follows a client-server architecture built on JSON-RPC 2.0, inspired by the Language Server Protocol (LSP) that powers IDE features. Three key roles:

  • Host: The AI application (Claude Desktop, Cursor, your custom agent) that runs the model and manages connections
  • Client: A component within the host that maintains a 1:1 connection with a server
  • Server: A program exposing tools, data, or prompts to clients

Servers expose three primitives:

PrimitiveControlPurposeExample
ToolsModel-controlledExecutable functions the LLM can invokeDatabase queries, API calls
ResourcesApplication-controlledRead-only data exposed to the modelFile contents, schemas
PromptsUser-controlledReusable templates for workflowsCode review checklists

Two transport mechanisms handle communication:

  • stdio: Server runs as a local subprocess via stdin/stdout. Fast, no network overhead. Ideal for Claude Desktop integrations.
  • Streamable HTTP: HTTP POST with Server-Sent Events for remote deployments. Supports multiple concurrent clients.

Building an MCP Server in Python

The Python SDK gives you FastMCP, which handles the protocol details so you don’t have to:

pip install "mcp[cli]"
# server.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("MyServer")

@mcp.tool()
def query_database(query: str, limit: int = 100) -> str:
    """Execute a read-only SQL query against the database."""
    # In production, execute against actual database
    return f"Results for: {query} (limit {limit})"

@mcp.resource("config://{key}")
def get_config(key: str) -> str:
    """Read configuration values."""
    configs = {"api_url": "https://api.example.com", "timeout": "30"}
    return configs.get(key, "not found")

@mcp.prompt()
def review_code(language: str = "python") -> str:
    """Generate a code review prompt."""
    return f"Review this {language} code for bugs, security issues, and style."

if __name__ == "__main__":
    mcp.run()  # stdio transport by default

For production HTTP deployments:

if __name__ == "__main__":
    mcp.run(transport="streamable-http", port=8000)

Test with the MCP Inspector: mcp dev server.py

Critical: For stdio servers, never print to stdout. It corrupts the JSON-RPC stream. Use stderr or a logging library configured to avoid stdout.

Building an MCP Server in TypeScript

npm install @modelcontextprotocol/sdk zod
// src/index.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

const server = new McpServer({
    name: 'my-server',
    version: '1.0.0'
});

server.registerTool(
    'query_database',
    {
        description: 'Execute a read-only SQL query',
        inputSchema: {
            query: z.string().describe('SQL SELECT statement'),
            limit: z.number().default(100)
        }
    },
    async ({ query, limit }) => ({
        content: [{ type: 'text', text: `Results for: ${query}` }]
    })
);

const transport = new StdioServerTransport();
await server.connect(transport);

Configuring Claude Desktop

Add servers to your config file. On macOS: ~/Library/Application Support/Claude/claude_desktop_config.json. On Windows: %APPDATA%\Claude\claude_desktop_config.json.

{
  "mcpServers": {
    "my-python-server": {
      "command": "python",
      "args": ["/path/to/server.py"],
      "env": { "DATABASE_URL": "postgres://..." }
    },
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/projects"]
    }
  }
}

Claude Desktop launches these on startup and the tools show up in your conversations. If a server doesn’t appear, it’s almost always because the command path isn’t what you think it is.

The Ecosystem

Major platforms support it:

ClientMCP Support
Claude DesktopFull native support
ChatGPTDeveloper Mode (September 2025)
CursorNative integration, one-click installs
VS CodeGitHub Copilot MCP extension
GeminiConfirmed by Google DeepMind
Windows 11Native Microsoft Copilot integration

Pre-built servers cover common integrations:

  • Official reference servers: Filesystem, Git, Memory, Fetch, Time
  • Company-maintained: GitHub, Microsoft Azure, AWS, Cloudflare, Stripe, Atlassian, MongoDB, Docker
  • Community registries: MCP.so, Smithery.ai, PulseMCP

When to Choose MCP vs Alternatives

Use MCP when:

  • Building production systems with 5+ tool integrations
  • Tools need to work across multiple AI providers
  • Security isolation between tools is critical
  • You want reusable integrations shared across teams

Use function calling when:

  • Prototyping with 1-3 custom tools
  • Working within a single LLM provider’s ecosystem
  • Quick iteration matters more than long-term maintainability

Use direct API integration when:

  • Ultra-low latency requirements (MCP adds protocol overhead)
  • Non-LLM applications
  • Legacy systems where MCP servers would require significant adaptation

They complement each other. Function calling is how the model says what it wants, MCP is how that request finds something to run it.

Production Best Practices

Single responsibility: Each server should do one thing. It’s tempting to build one server that does everything for your company, and you’ll regret it the first time you want to give somebody access to half of it.

Error handling: Return errors in the response with isError: true. Implement retry with exponential backoff:

import asyncio
import random

async def retry_with_backoff(operation, max_attempts=3):
    for attempt in range(max_attempts):
        try:
            return await operation()
        except Exception as e:
            if attempt == max_attempts - 1:
                raise
            delay = 2 ** attempt + random.uniform(0, 1)
            await asyncio.sleep(delay)

Security: Remote servers must use OAuth 2.1 with recognized certificates. Provide tool annotations (readOnlyHint, destructiveHint) so hosts can implement consent flows. Never collect extraneous conversation data.

Observability: Use structured logging with correlation IDs. Track throughput, latency percentiles (p50/p95/p99), and error rates.

Current Limitations

Context window pressure: Every tool definition is loaded upfront and every one costs tokens. Past a few dozen tools it degrades, and you’ll want progressive disclosure rather than handing the model the whole catalog.

OAuth 2.1 adoption lag: The spec defines OAuth 2.1 for authentication, but many clients haven’t fully implemented it yet.

Enterprise compliance gaps: Native audit trails, RBAC, and data residency controls are thin. If you’re carrying GDPR or SOX obligations you’re building that layer yourself.

Stateful scaling: The protocol assumes stateful sessions by default. Use Streamable HTTP transport with external session stores for horizontal scaling.

What I’m still unsure about

MCP solves a real problem and it solves it in the boring, correct way, which is why it spread. Standardizing the connection layer doesn’t make anything smarter. It removes a category of work that was never interesting to begin with, and that’s a good enough reason for a protocol to exist.

The part I can’t call yet is whether the primitives survive contact with scale. Tools are clearly right. Resources and Prompts are used far less in practice than the spec implies they should be, and a protocol with a primitive that nobody reaches for usually ends up dropping it or redefining it. Context window pressure is pushing everyone toward loading tools on demand, which is a sensible fix and also an admission that the original design assumed a smaller world than the one it got.

If you want to form your own view, the fastest route isn’t the spec. It’s connecting Claude Desktop to the filesystem server and watching what actually crosses the wire.