In late 2024, Anthropic open-sourced a protocol that quietly changed how AI agents work. Model Context Protocol (MCP) is a standardized way for LLMs to connect to tools, data sources, and services. By mid-2026 it has become the default integration layer for serious agentic systems. If you are building anything with AI agents and you are not using MCP, you are reinventing the wheel.
This post is a complete technical breakdown: what MCP is, how it differs from raw tool calling, and a step-by-step guide to building your own MCP server in Python.
What MCP Actually Is
Before MCP, every AI integration was a bespoke mess. You had function calling in OpenAI format, tool use in Anthropic format, plugins in the old ChatGPT format, and a dozen SDK-specific patterns on top. Each one required custom glue code. MCP replaces all of it with a single open protocol.
The model is: your AI client (Claude, GPT, any LLM) speaks MCP. Your tools (filesystem, database, browser, APIs) expose an MCP server. The client discovers capabilities at runtime, calls them, gets structured results back. No custom adapters, no format mismatches.
| Approach | Integration | Schema definition | Portability |
|---|---|---|---|
| Pre-MCP tool calling | Custom per-provider | Function schemas per SDK | Breaks on model switch |
| MCP | Standardized open protocol | Discover at runtime | Works across any MCP-compatible client |
The Three Primitives: Tools, Resources, Prompts
MCP exposes three types of capabilities from a server:
- -Tools - functions the LLM can call. Read a file, query a database, send an HTTP request. Tools take parameters and return results. This is the main building block.
- -Resources - data the LLM can read. Think of these as context providers: a file, a database row, a URL. Resources are identified by URI and streamed to the model as context.
- -Prompts - pre-built prompt templates exposed by the server. Useful for standardized workflows like "summarize this document" or "review this code."
In practice, 90% of MCP usage is Tools. Resources and Prompts matter more for complex multi-agent orchestration where context management is explicit.
Building an MCP Server in Python
The official Python SDK makes this straightforward. Here is a minimal MCP server that exposes three tools: read a file, list a directory, and run a shell command.
pip install mcp# server.py
import asyncio
import subprocess
from pathlib import Path
from mcp.server import Server
from mcp.server.models import InitializationOptions
from mcp.server.stdio import stdio_server
from mcp import types
server = Server("filesystem-tools")
@server.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="read_file",
description="Read the contents of a file",
inputSchema={
"type": "object",
"properties": {
"path": {"type": "string", "description": "Absolute file path"},
},
"required": ["path"],
},
),
types.Tool(
name="list_directory",
description="List files and directories at a path",
inputSchema={
"type": "object",
"properties": {
"path": {"type": "string", "description": "Directory path"},
},
"required": ["path"],
},
),
types.Tool(
name="run_command",
description="Run a shell command and return stdout",
inputSchema={
"type": "object",
"properties": {
"command": {"type": "string", "description": "Shell command to run"},
},
"required": ["command"],
},
),
]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
if name == "read_file":
path = Path(arguments["path"])
if not path.exists():
return [types.TextContent(type="text", text=f"Error: {path} not found")]
return [types.TextContent(type="text", text=path.read_text())]
elif name == "list_directory":
path = Path(arguments["path"])
entries = [str(p.name) + ("/" if p.is_dir() else "") for p in sorted(path.iterdir())]
return [types.TextContent(type="text", text="\n".join(entries))]
elif name == "run_command":
result = subprocess.run(
arguments["command"], shell=True, capture_output=True, text=True, timeout=30
)
output = result.stdout or result.stderr or "(no output)"
return [types.TextContent(type="text", text=output)]
return [types.TextContent(type="text", text=f"Unknown tool: {name}")]
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
InitializationOptions(
server_name="filesystem-tools",
server_version="0.1.0",
capabilities=server.get_capabilities(
notification_options=None, experimental_capabilities={}
),
),
)
if __name__ == "__main__":
asyncio.run(main())Connecting to Claude Desktop
Once the server is written, you register it in the Claude Desktop config file. Claude then discovers and uses your tools automatically during any conversation.
// claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/)
{
"mcpServers": {
"filesystem-tools": {
"command": "python",
"args": ["/absolute/path/to/server.py"]
}
}
}Restart Claude Desktop and your tools appear in the tool picker. Claude can now read your local files, list directories, and run shell commands on your behalf without any additional integration code.
MCP Over HTTP: Stateless Servers for Production
The stdio transport used above is for local tools. For production services that need to be called from cloud-hosted agents, MCP supports an HTTP+SSE transport. This is how you expose an MCP server as a web service:
from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
from starlette.routing import Route, Mount
import uvicorn
sse = SseServerTransport("/messages/")
async def handle_sse(request):
async with sse.connect_sse(
request.scope, request.receive, request._send
) as streams:
await server.run(
streams[0], streams[1],
InitializationOptions(
server_name="my-service",
server_version="1.0.0",
capabilities=server.get_capabilities(
notification_options=None,
experimental_capabilities={},
),
),
)
app = Starlette(
routes=[
Route("/sse", endpoint=handle_sse),
Mount("/messages/", app=sse.handle_post_message),
]
)
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)This pattern lets you deploy an MCP server on Render, Railway, or any cloud host and connect to it from any MCP client anywhere. The server is stateless per request, scales horizontally, and works with any LLM that speaks MCP.
Real-World MCP Patterns I Use in Production
Database Read Tool
Expose a SQL query tool that lets Claude inspect your production database read-only. Claude can answer questions about your data without you writing custom analysis scripts for every question.
@server.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "query_db":
conn = await asyncpg.connect(os.getenv("DATABASE_URL"))
# Force read-only: wrap in a transaction and rollback
async with conn.transaction():
rows = await conn.fetch(arguments["sql"])
await conn.execute("ROLLBACK")
result = [dict(row) for row in rows]
return [types.TextContent(type="text", text=json.dumps(result, indent=2))]Authenticated API Wrapper
Wrap a third-party API with an MCP tool so the LLM can call it without ever seeing your credentials. The server holds the API key; the LLM only receives the response.
Browser Control via Playwright
Expose navigate, click, and extract tools backed by a headless Playwright instance. The LLM can browse the web, fill forms, and extract structured data from any page. This is the pattern behind most serious AI scraping agents in 2026.
Why MCP Won Over Competing Approaches
The alternative to MCP is either raw function calling (which is provider-specific and breaks when you switch models) or custom agent frameworks that define their own tool interfaces (which lock you into that framework). MCP is neither: it is a transport-level protocol that any LLM can implement and any service can expose.
The key design decision that made it win: MCP servers are just processes. They communicate over stdin/stdout or HTTP. You do not need an SDK on the server side if you implement the JSON-RPC protocol directly. This made adoption fast because any existing service could add an MCP interface in a few hours.
MCP in 2026: The Ecosystem
As of mid-2026, the MCP ecosystem includes hundreds of official and community servers: GitHub, Slack, Notion, PostgreSQL, browser control, vector databases, code execution, and more. Claude Desktop, Claude Code, and most serious agent frameworks use MCP as the default tool interface. If you are building a service that AI agents might need to use, publishing an MCP server for it is now standard practice.
Should I use MCP or raw tool calling?
If you are building for a single model and a single deployment context, raw tool calling is simpler. If you are building tools that will be used across different models, different clients, or exposed to third-party agents, use MCP. The protocol overhead is minimal and the portability is worth it.
Does MCP work with models other than Claude?
Yes. MCP is model-agnostic. OpenAI, Google, and open-source frameworks like LangChain and LlamaIndex all have MCP client support in 2026. The server you write today works with GPT-5, Gemini, and any future model that adopts the protocol.
