Model Context Protocol

Tools · Resources · Prompts // JSON-RPC 2.0 // stdio · SSE

MCP Architecture

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 / SSEstdio / SSE
   ┌────────▼────────┐      ┌────────▼────────┐
   │  MCP Server      │      │  MCP Server      │
   │                 │      │                 │
   │  tools          │      │  tools          │
   │  resources      │      │  resources      │
   │  prompts        │      │  prompts        │
   └─────────────────┘      └─────────────────┘
        filesystem                database / API
RoleResponsibilitiesExample
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

The 3 Primitives

Everything a server exposes falls into one of three capability types. A server can implement any combination.

Tools
Model-controlled actions
Functions the LLM can call to perform actions or retrieve computed data. The model decides when to invoke them based on context.
tools/list → tool definitions
tools/call → result | error
Resources
App-controlled data
Static or dynamic data the application exposes to the LLM's context. Addressed by URI. The host decides what to attach.
resources/list → resource list
resources/read → contents
resources/subscribe → live updates
Prompts
User-controlled templates
Reusable prompt templates with argument slots. Surface in the host UI (slash commands, etc.) for users to invoke directly.
prompts/list → prompt list
prompts/get → messages array

Transport Layer

MCP is transport-agnostic. The two standard transports cover local processes and remote HTTP services.

stdio

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": "..." }
    }
  }
}

SSE (HTTP)

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"
    }
  }
}
Streamable HTTP (2025-03-26+) The newer spec replaces SSE with a single 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.

Tool Schema

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
  }
}
Tool errors vs protocol errors A tool can fail in two ways. A tool-level error returns a normal JSON-RPC result with 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.

AnnotationTypeMeaning
readOnlyHintboolTool does not modify external state
destructiveHintboolMay perform destructive actions (delete, overwrite)
idempotentHintboolRepeated calls with same args have no extra effect
openWorldHintboolInteracts with external systems (network, etc.)
with annotations
{
  "name": "delete_file",
  "annotations": {
    "destructiveHint": true,
    "readOnlyHint":   false
  },
  "inputSchema": { ... }
}

Resources

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..."
}

Resource Templates

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"
}

Prompts

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
```"
      }
    }
  ]
}

Connection Lifecycle

Every MCP session follows a strict initialization handshake before capability exchange. Both sides declare supported protocol versions and capabilities.

Client
initialize
protocolVersion + capabilities
Server
initialize
serverInfo + capabilities
Client
initialized
notification (no response)
Both
Ready
tools / resources / prompts
Either
shutdown
graceful close
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"
    }
  }
}
Capability negotiation A server advertising 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.

Error Codes

MCP uses standard JSON-RPC 2.0 error codes plus its own range.

CodeNameWhen
-32700Parse errorInvalid JSON received
-32600Invalid requestJSON is not a valid request object
-32601Method not foundCalled method doesn't exist / not supported
-32602Invalid paramsInvalid method parameters
-32603Internal errorInternal JSON-RPC error
-32000 to -32099Server errorImplementation-defined server errors
Don't confuse protocol errors with tool errors Protocol 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.

SDK Patterns

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);

Quick Reference

All Methods

MethodDirection
initializeclient → server
tools/listclient → server
tools/callclient → server
resources/listclient → server
resources/readclient → server
resources/subscribeclient → server
prompts/listclient → server
prompts/getclient → server
sampling/createMessageserver → client
roots/listserver → client
pingeither

Notifications

NotificationSender
notifications/initializedclient
notifications/roots/list_changedclient
notifications/tools/list_changedserver
notifications/resources/list_changedserver
notifications/resources/updatedserver
notifications/prompts/list_changedserver
notifications/progresseither
notifications/messageeither (logging)
notifications/cancelledeither

Content Types

TypeFields
texttext: string
imagedata: base64, mimeType
resourceresource: { uri, text|blob }

Protocol Versions

VersionKey additions
2024-11-05Initial stable release. Tools, resources, prompts, sampling.
2025-03-26Streamable HTTP transport, tool annotations, OAuth 2.1 auth, resource links in tool results.
// model context protocol // tools · resources · prompts //