MCP is an open protocol for connecting LLMs to external tools, data sources, and services. A Host (Claude Desktop, an IDE, your app) embeds one or more Clients; each Client maintains a 1:1 connection to a Server that exposes capabilities.
┌──────────────────────────────────────────────────────────────┐ │ HOST (Claude Desktop / IDE / custom application) │ │ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ MCP Client │ │ MCP Client │ ... │ │ └────────┬────────┘ └────────┬────────┘ │ └───────────┼────────────────────────┼───────────────────────┘ │ stdio / SSE │ stdio / SSE ┌────────▼────────┐ ┌────────▼────────┐ │ MCP Server │ │ MCP Server │ │ │ │ │ │ tools │ │ tools │ │ resources │ │ resources │ │ prompts │ │ prompts │ └─────────────────┘ └─────────────────┘ filesystem database / API
| Role | Responsibilities | Example |
|---|---|---|
| Host | Owns LLM context, manages client connections, enforces security policy | Claude Desktop, VS Code, your app |
| Client | Maintains one server connection, forwards capability calls from host to server | Embedded in host process |
| Server | Exposes tools, resources, prompts over a transport. Stateless or stateful. | Your MCP server process |
Everything a server exposes falls into one of three capability types. A server can implement any combination.
MCP is transport-agnostic. The two standard transports cover local processes and remote HTTP services.
local process # Server reads from stdin, writes to stdout # Host launches server as subprocess # claude_desktop_config.json { "mcpServers": { "my-server": { "command": "python", "args": ["-m", "my_mcp_server"], "env": { "API_KEY": "..." } } } }
remote / network # Server exposes two endpoints: # GET /sse — event stream (server → client) # POST /message — messages (client → server) # Config { "mcpServers": { "remote": { "url": "https://example.com/sse" } } }
POST /mcp endpoint that can return either a single JSON response or an SSE stream. Older clients use legacy SSE; check spec version via protocolVersion in the initialize handshake.
Tools are defined with a name, description, and a JSON Schema for their inputs. The description is critical — the LLM uses it to decide when and how to call the tool.
JSON Schema { "name": "read_file", "description": "Read the contents of a file at a given path", "inputSchema": { "type": "object", "properties": { "path": { "type": "string", "description": "Absolute path to the file" }, "encoding": { "type": "string", "enum": ["utf-8", "base64"], "default": "utf-8" } }, "required": ["path"] } }
request { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "read_file", "arguments": { "path": "/etc/hosts" } } }
response { "jsonrpc": "2.0", "id": 1, "result": { "content": [ { "type": "text", "text": "127.0.0.1 localhost\n..." } ], "isError": false } }
isError: true and error text in content — the LLM sees this and can react. A protocol error returns a JSON-RPC error object — used for invalid calls, unknown tools, malformed requests.
tool-level error (LLM sees it) { "result": { "content": [{ "type": "text", "text": "File not found: /etc/foo" }], "isError": true } }
protocol error { "error": { "code": -32601, "message": "Method not found", "data": "unknown tool: read_file" } }
Tool annotations hint at behavior to help hosts make security and UX decisions. They are advisory only — hosts must not rely on them for enforcement.
| Annotation | Type | Meaning |
|---|---|---|
| readOnlyHint | bool | Tool does not modify external state |
| destructiveHint | bool | May perform destructive actions (delete, overwrite) |
| idempotentHint | bool | Repeated calls with same args have no extra effect |
| openWorldHint | bool | Interacts with external systems (network, etc.) |
with annotations { "name": "delete_file", "annotations": { "destructiveHint": true, "readOnlyHint": false }, "inputSchema": { ... } }
Resources expose data to the LLM context. Each resource has a URI, a human-readable name, and optional MIME type. Resources can be static files or dynamically generated.
list response { "resources": [ { "uri": "file:///project/README.md", "name": "Project README", "description": "Project overview", "mimeType": "text/markdown" }, { "uri": "db://users/active", "name": "Active Users", "mimeType": "application/json" } ] }
read response { "contents": [ { "uri": "file:///project/README.md", "mimeType": "text/markdown", "text": "# My Project\n..." } ] } // Binary content uses blob + base64 { "uri": "file:///image.png", "mimeType": "image/png", "blob": "iVBORw0KGgo..." }
URI templates (RFC 6570) let you define parametric resources without enumerating every possible URI.
resourceTemplates { "uriTemplate": "file:///{path}", "name": "File system access", "mimeType": "text/plain" } { "uriTemplate": "db://users/{id}/profile", "name": "User profile", "mimeType": "application/json" }
Prompt templates are predefined message sequences with typed argument slots. Hosts surface these as slash commands or UI elements for direct user invocation.
prompt definition { "name": "review_code", "description": "Review code for issues", "arguments": [ { "name": "language", "description": "Programming language", "required": true }, { "name": "focus", "description": "security | performance | style", "required": false } ] }
get response { "messages": [ { "role": "user", "content": { "type": "text", "text": "Review this Python code focusing on security issues: ```python # user's code injected here ```" } } ] }
Every MCP session follows a strict initialization handshake before capability exchange. Both sides declare supported protocol versions and capabilities.
initialize request (client) { "method": "initialize", "params": { "protocolVersion": "2025-03-26", "capabilities": { "roots": { "listChanged": true }, "sampling": {} }, "clientInfo": { "name": "MyApp", "version": "1.0.0" } } }
initialize response (server) { "result": { "protocolVersion": "2025-03-26", "capabilities": { "tools": { "listChanged": true }, "resources": { "subscribe": true }, "prompts": { "listChanged": true } }, "serverInfo": { "name": "my-server", "version": "0.1.0" } } }
tools: {} means it supports tools but not list-change notifications. tools: { listChanged: true } means the server will send notifications/tools/list_changed when its tool list changes. Clients should only use capabilities the other side declared.
MCP uses standard JSON-RPC 2.0 error codes plus its own range.
| Code | Name | When |
|---|---|---|
| -32700 | Parse error | Invalid JSON received |
| -32600 | Invalid request | JSON is not a valid request object |
| -32601 | Method not found | Called method doesn't exist / not supported |
| -32602 | Invalid params | Invalid method parameters |
| -32603 | Internal error | Internal JSON-RPC error |
| -32000 to -32099 | Server error | Implementation-defined server errors |
-32xxx) mean something went wrong at the transport/protocol level. Tool errors use isError: true in the tool result content — the LLM receives and reasons about these. Use tool-level errors for expected failure cases (file not found, rate limit hit, invalid input). Use protocol errors only for malformed requests.
Both official SDKs follow the same pattern: create a server, register handlers, connect a transport.
Python from mcp.server.fastmcp import FastMCP mcp = FastMCP("my-server") # Tool @mcp.tool() def read_file(path: str) -> str: """Read a file and return its contents.""" with open(path) as f: return f.read() # Resource @mcp.resource("config://app") def get_config() -> str: """Current application config.""" return config.to_json() # Prompt @mcp.prompt() def review_code(language: str, code: str) -> str: return f"Review this {language} code:\n\n{code}" if __name__ == "__main__": mcp.run() # stdio by default
Python low-level from mcp.server import Server from mcp.server.stdio import stdio_server import mcp.types as types server = Server("my-server") @server.list_tools() async def handle_list_tools() -> list[types.Tool]: return [ types.Tool( name="read_file", description="Read a file", inputSchema={ "type": "object", "properties": { "path": { "type": "string" } }, "required": ["path"] } ) ] @server.call_tool() async def handle_call_tool(name: str, arguments: dict): if name == "read_file": content = open(arguments["path"]).read() return [types.TextContent(type="text", text=content)] raise ValueError(f"Unknown tool: {name}") async def main(): async with stdio_server() as (r, w): await server.run(r, w, server.create_initialization_options())
TypeScript import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ListToolsRequestSchema, CallToolRequestSchema } from "..."; const server = new Server( { name: "my-server", version: "1.0.0" }, { capabilities: { tools: {} } } ); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [{ name: "read_file", description: "Read a file", inputSchema: { type: "object", properties: { path: { type: "string" } }, required: ["path"] } }] })); server.setRequestHandler(CallToolRequestSchema, async (req) => { if (req.params.name === "read_file") { const content = fs.readFileSync(req.params.arguments.path, "utf-8"); return { content: [{ type: "text", text: content }] }; } throw new Error(`Unknown tool: ${req.params.name}`); }); const transport = new StdioServerTransport(); await server.connect(transport);
| Method | Direction |
|---|---|
| initialize | client → server |
| tools/list | client → server |
| tools/call | client → server |
| resources/list | client → server |
| resources/read | client → server |
| resources/subscribe | client → server |
| prompts/list | client → server |
| prompts/get | client → server |
| sampling/createMessage | server → client |
| roots/list | server → client |
| ping | either |
| Notification | Sender |
|---|---|
| notifications/initialized | client |
| notifications/roots/list_changed | client |
| notifications/tools/list_changed | server |
| notifications/resources/list_changed | server |
| notifications/resources/updated | server |
| notifications/prompts/list_changed | server |
| notifications/progress | either |
| notifications/message | either (logging) |
| notifications/cancelled | either |
| Type | Fields |
|---|---|
| text | text: string |
| image | data: base64, mimeType |
| resource | resource: { uri, text|blob } |
| Version | Key additions |
|---|---|
| 2024-11-05 | Initial stable release. Tools, resources, prompts, sampling. |
| 2025-03-26 | Streamable HTTP transport, tool annotations, OAuth 2.1 auth, resource links in tool results. |