MCP Protocol
The Model Context Protocol (MCP) is an open protocol that standardizes how applications provide context and tools to large language models. Think of MCP as the USB-C for AI applications — it provides a universal interface for connecting LLMs with external data sources, tools, and services.
MCP was designed to solve the fragmentation problem in AI tooling. Before MCP, every LLM integration required custom code — a custom API wrapper for databases, another for file systems, another for web search. MCP provides a single, standardized protocol that any MCP-compatible server can implement and any MCP-compatible client can consume.
info
MCP follows a client-server architecture with three core components: hosts initiate connections, clients maintain sessions, and servers provide capabilities. The protocol defines how these components discover, negotiate, and communicate capabilities.
Core Concepts
Host — The application or environment that initiates the connection (e.g., Claude Desktop, an IDE, a custom web app). The host is responsible for user authentication and authorization.
Client — A stateful session within the host that maintains a 1:1 connection with a server. The client handles message routing, capability negotiation, and lifecycle management.
Server — A lightweight process that exposes specific capabilities (tools, resources, prompts). Each server operates independently and can be composed to provide complex functionality.
Transport — The underlying communication mechanism. MCP supports stdio (for local processes) and Server-Sent Events over HTTP (for remote services).
| Capability | Description | Example |
|---|---|---|
| Tools | Executable actions that the server can perform (with user approval) | Search web, query database, send email |
| Resources | Exposed data or content that can be read by the client | Files, database records, API responses |
| Prompts | Pre-defined prompt templates with dynamic arguments | Code review prompt, summarization template |
| Sampling | Server requests LLM generation from the client (server-to-client) | Agentic loops, delegated reasoning |
note
MCP defines two transport mechanisms. The choice depends on whether the server runs locally or remotely.
stdio Transport
The server runs as a child process of the client, communicating over standard input/output. This is the simplest deployment model — the client spawns the server, sends JSON-RPC messages on stdin, and reads responses from stdout.
| 1 | # stdio server: reads from stdin, writes to stdout |
| 2 | import sys |
| 3 | import json |
| 4 | |
| 5 | def handle_request(request: dict) -> dict: |
| 6 | method = request.get("method") |
| 7 | if method == "tools/list": |
| 8 | return { |
| 9 | "jsonrpc": "2.0", |
| 10 | "id": request["id"], |
| 11 | "result": { |
| 12 | "tools": [{ |
| 13 | "name": "get_weather", |
| 14 | "description": "Get current weather", |
| 15 | "inputSchema": { |
| 16 | "type": "object", |
| 17 | "properties": { |
| 18 | "location": {"type": "string"} |
| 19 | } |
| 20 | } |
| 21 | }] |
| 22 | } |
| 23 | } |
| 24 | # ... handle other methods |
| 25 | |
| 26 | for line in sys.stdin: |
| 27 | request = json.loads(line) |
| 28 | response = handle_request(request) |
| 29 | sys.stdout.write(json.dumps(response) + "\n") |
| 30 | sys.stdout.flush() |
SSE Transport (HTTP)
For remote servers, MCP uses Server-Sent Events over HTTP. The client connects to a URL, receives events on an SSE stream, and sends requests via HTTP POST. This enables MCP to work across network boundaries.
| 1 | # SSE server using FastAPI |
| 2 | from fastapi import FastAPI, Request |
| 3 | from fastapi.responses import StreamingResponse |
| 4 | import asyncio |
| 5 | import json |
| 6 | |
| 7 | app = FastAPI() |
| 8 | |
| 9 | @app.post("/mcp") |
| 10 | async def mcp_endpoint(request: Request): |
| 11 | body = await request.json() |
| 12 | method = body.get("method") |
| 13 | |
| 14 | if method == "tools/call": |
| 15 | tool_name = body["params"]["name"] |
| 16 | args = body["params"]["arguments"] |
| 17 | result = await execute_tool(tool_name, args) |
| 18 | return {"jsonrpc": "2.0", |
| 19 | "id": body["id"], |
| 20 | "result": {"content": result}} |
| 21 | # ... other methods |
| 22 | |
| 23 | @app.get("/sse") |
| 24 | async def sse_endpoint(): |
| 25 | async def event_stream(): |
| 26 | while True: |
| 27 | # Send server events (e.g., tool responses) |
| 28 | yield f"data: {json.dumps({'type': 'ping'})}\n\n" |
| 29 | await asyncio.sleep(30) |
| 30 | return StreamingResponse(event_stream(), |
| 31 | media_type="text/event-stream") |
best practice
Tools are the primary way MCP servers expose executable functionality. Each tool has a name, description, and JSON Schema input definition. The client discovers available tools via the tools/list method and invokes them via tools/call.
| 1 | # Complete MCP server with tools |
| 2 | from mcp.server import Server, NotificationOptions |
| 3 | from mcp.server.models import InitializationOptions |
| 4 | import mcp.server.stdio |
| 5 | |
| 6 | async def main(): |
| 7 | server = Server("data-tools") |
| 8 | |
| 9 | @server.list_tools() |
| 10 | async def list_tools(): |
| 11 | return [ |
| 12 | { |
| 13 | "name": "query_database", |
| 14 | "description": "Run SQL query on the analytics DB", |
| 15 | "inputSchema": { |
| 16 | "type": "object", |
| 17 | "properties": { |
| 18 | "sql": { |
| 19 | "type": "string", |
| 20 | "description": "SQL query to execute" |
| 21 | }, |
| 22 | "limit": { |
| 23 | "type": "integer", |
| 24 | "default": 100 |
| 25 | } |
| 26 | }, |
| 27 | "required": ["sql"] |
| 28 | } |
| 29 | }, |
| 30 | { |
| 31 | "name": "fetch_url", |
| 32 | "description": "Fetch content from a URL", |
| 33 | "inputSchema": { |
| 34 | "type": "object", |
| 35 | "properties": { |
| 36 | "url": {"type": "string"}, |
| 37 | "format": { |
| 38 | "type": "string", |
| 39 | "enum": ["text", "html", "markdown"] |
| 40 | } |
| 41 | }, |
| 42 | "required": ["url"] |
| 43 | } |
| 44 | } |
| 45 | ] |
| 46 | |
| 47 | @server.call_tool() |
| 48 | async def call_tool(name: str, arguments: dict): |
| 49 | if name == "query_database": |
| 50 | results = await run_query(arguments["sql"]) |
| 51 | return {"content": str(results)} |
| 52 | elif name == "fetch_url": |
| 53 | content = await fetch_url(arguments["url"]) |
| 54 | return {"content": content} |
| 55 | |
| 56 | async with mcp.server.stdio.stdio_server() as (read, write): |
| 57 | await server.run(read, write, |
| 58 | InitializationOptions( |
| 59 | server_name="data-tools", |
| 60 | server_version="1.0.0" |
| 61 | )) |
| 62 | |
| 63 | if __name__ == "__main__": |
| 64 | import asyncio |
| 65 | asyncio.run(main()) |
info
Resources expose data to the LLM. Unlike tools (which perform actions), resources are read-only data sources. They can be static (like a documentation file) or dynamic (like the contents of a specific database record identified by a URI template).
| 1 | # Resource exposure with templates |
| 2 | @server.list_resources() |
| 3 | async def list_resources(): |
| 4 | return [ |
| 5 | { |
| 6 | "uri": "docs://mcp", |
| 7 | "name": "MCP Documentation", |
| 8 | "mimeType": "text/markdown" |
| 9 | }, |
| 10 | { |
| 11 | "uri": "docs://rag", |
| 12 | "name": "RAG Guide", |
| 13 | "mimeType": "text/markdown" |
| 14 | } |
| 15 | ] |
| 16 | |
| 17 | @server.read_resource() |
| 18 | async def read_resource(uri: str): |
| 19 | if uri.startswith("docs://"): |
| 20 | doc_name = uri.replace("docs://", "") |
| 21 | content = load_documentation(doc_name) |
| 22 | return { |
| 23 | "contents": [{ |
| 24 | "uri": uri, |
| 25 | "mimeType": "text/markdown", |
| 26 | "text": content |
| 27 | }] |
| 28 | } |
| 29 | |
| 30 | # Resource templates for dynamic access |
| 31 | @server.list_resource_templates() |
| 32 | async def list_templates(): |
| 33 | return [ |
| 34 | { |
| 35 | "uriTemplate": |
| 36 | "analytics://reports/{report_id}", |
| 37 | "name": "Analytics Report", |
| 38 | "description": |
| 39 | "Access a specific analytics report by ID", |
| 40 | "mimeType": "application/json" |
| 41 | } |
| 42 | ] |
note
MCP servers can expose reusable prompt templates. These are predefined prompts with dynamic arguments that clients can present as ready-to-use interactions. Prompt templates are particularly useful for standardizing common AI workflows.
| 1 | # Prompt template definitions |
| 2 | @server.list_prompts() |
| 3 | async def list_prompts(): |
| 4 | return [ |
| 5 | { |
| 6 | "name": "code_review", |
| 7 | "description": |
| 8 | "Review code changes and suggest improvements", |
| 9 | "arguments": [ |
| 10 | { |
| 11 | "name": "code", |
| 12 | "description": |
| 13 | "The code snippet to review", |
| 14 | "required": True |
| 15 | }, |
| 16 | { |
| 17 | "name": "language", |
| 18 | "description": |
| 19 | "Programming language", |
| 20 | "required": False |
| 21 | } |
| 22 | ] |
| 23 | }, |
| 24 | { |
| 25 | "name": "summarize_doc", |
| 26 | "description": |
| 27 | "Summarize a document at a specified level", |
| 28 | "arguments": [ |
| 29 | { |
| 30 | "name": "document", |
| 31 | "description": "Document text to summarize", |
| 32 | "required": True |
| 33 | }, |
| 34 | { |
| 35 | "name": "style", |
| 36 | "description": "Summary style: brief/detailed", |
| 37 | "required": False |
| 38 | } |
| 39 | ] |
| 40 | } |
| 41 | ] |
| 42 | |
| 43 | @server.get_prompt() |
| 44 | async def get_prompt(name: str, arguments: dict): |
| 45 | if name == "code_review": |
| 46 | lang = arguments.get("language", "unknown") |
| 47 | return { |
| 48 | "messages": [ |
| 49 | { |
| 50 | "role": "user", |
| 51 | "content": { |
| 52 | "type": "text", |
| 53 | "text": f"Review this {lang} code:\n" |
| 54 | f"\n{arguments['code']}" |
| 55 | } |
| 56 | } |
| 57 | ] |
| 58 | } |
info
An MCP client connects to a server, discovers its capabilities, and invokes tools or reads resources. Here is a minimal client implementation.
| 1 | import asyncio |
| 2 | import json |
| 3 | import subprocess |
| 4 | import uuid |
| 5 | |
| 6 | class MCPClient: |
| 7 | def __init__(self, command: list[str]): |
| 8 | self.process = subprocess.Popen( |
| 9 | command, |
| 10 | stdin=subprocess.PIPE, |
| 11 | stdout=subprocess.PIPE, |
| 12 | stderr=subprocess.PIPE, |
| 13 | text=True |
| 14 | ) |
| 15 | self.pending = {} |
| 16 | |
| 17 | async def send_request(self, method: str, |
| 18 | params: dict = None) -> dict: |
| 19 | request_id = str(uuid.uuid4()) |
| 20 | request = { |
| 21 | "jsonrpc": "2.0", |
| 22 | "id": request_id, |
| 23 | "method": method, |
| 24 | "params": params or {} |
| 25 | } |
| 26 | self.process.stdin.write( |
| 27 | json.dumps(request) + "\n" |
| 28 | ) |
| 29 | self.process.stdin.flush() |
| 30 | |
| 31 | response = json.loads( |
| 32 | self.process.stdout.readline() |
| 33 | ) |
| 34 | return response.get("result") |
| 35 | |
| 36 | async def list_tools(self) -> list: |
| 37 | result = await self.send_request("tools/list") |
| 38 | return result["tools"] |
| 39 | |
| 40 | async def call_tool(self, name: str, |
| 41 | arguments: dict) -> dict: |
| 42 | result = await self.send_request("tools/call", { |
| 43 | "name": name, |
| 44 | "arguments": arguments |
| 45 | }) |
| 46 | return result |
| 47 | |
| 48 | def close(self): |
| 49 | self.process.terminate() |
| 50 | self.process.wait() |
| 51 | |
| 52 | # Usage |
| 53 | async def main(): |
| 54 | client = MCPClient(["python", "server.py"]) |
| 55 | tools = await client.list_tools() |
| 56 | print(f"Available tools: {tools}") |
| 57 | result = await client.call_tool("get_weather", |
| 58 | {"location": "Tokyo"}) |
| 59 | print(f"Result: {result}") |
| 60 | client.close() |
| 61 | |
| 62 | asyncio.run(main()) |
best practice
MCP servers execute arbitrary code on behalf of LLMs. This creates significant security surface area that must be addressed at the protocol, transport, and application levels.
User Confirmation
All tool invocations require explicit user approval. The host must never auto-execute tools. Present the tool name, arguments, and a clear description of what will happen before asking for confirmation.
Data Access Control
Resources should respect the host's authentication and authorization boundaries. An MCP server for file access must not allow reading files outside the allowed directory (path traversal protection).
Input Validation
Every tool parameter must be validated and sanitized. An LLM may inject SQL, shell commands, or malicious paths through tool arguments. Never pass user-generated arguments directly to system calls.
Transport Security
Remote MCP connections over SSE must use TLS (HTTPS). Authentication tokens should never be hardcoded — use environment variables or secure credential stores. Implement rate limiting to prevent abuse.
Sandboxing
Run MCP servers in sandboxed environments (containers, restricted users, seccomp profiles). A compromised MCP server should not be able to access the host system beyond its intended scope.
warning