Model Context Protocol (MCP)
MCP is an open protocol (Anthropic-led but vendor-neutral) for connecting AI assistants to external systems — databases, APIs, file systems, dev tools, monitoring dashboards. It’s the “USB-C of AI” pitch: one protocol, many servers, many clients. The protocol layer is JSON-RPC 2.0 over a few transports (stdio, HTTP, Server-Sent Events, streamable HTTP); on top of that, servers expose three primitives — tools (model-controlled functions), resources (application-controlled data), and prompts (user-controlled templates). MCP is the integration substrate used by Claude Code, Claude Desktop, Cursor, Continue, and dozens of other clients. This note covers the spec (2025-06-18 revision), the four transports, the three server primitives, capability negotiation, lifecycle, authorization, and the SDK landscape.
See also
- claude-code-cli —
claude mcpcommands +.mcp.jsonformat - claude-agent-sdk —
mcp_serversoption in the SDK - tool-use-and-function-calling — MCP tools surface to Claude as regular tools (with
mcp__<server>__<tool>naming) - microservices-patterns — MCP is essentially a microservice protocol with AI-specific primitives
1. The shape of the spec
Latest version: 2025-06-18 (as of this note’s creation date). Versions are date-stamped; protocol clients and servers negotiate on connect.
Five layers:
- Base protocol — JSON-RPC 2.0 message types
- Lifecycle — initialize → operate → shutdown
- Authorization — OAuth 2.0 + alternatives for HTTP transports
- Server features — tools, resources, prompts
- Client features — sampling, roots (workspace context)
Plus utilities: logging, completion, pagination, cancellation, progress.
All implementations MUST support base protocol + lifecycle. Everything else is opt-in via capability negotiation.
2. Base protocol — JSON-RPC 2.0
Three message types:
2.1 Request
{
jsonrpc: "2.0",
id: string | number, // must NOT be null, must be unique within session
method: string,
params?: { [key: string]: unknown }
}2.2 Response
{
jsonrpc: "2.0",
id: string | number, // matches the request id
result?: { ... }, // exactly one of result | error
error?: { code: integer, message: string, data?: unknown }
}2.3 Notification (one-way, no response)
{
jsonrpc: "2.0",
method: string,
params?: { ... }
// NO id field
}2.4 _meta reserved field
Both requests/responses can include _meta for protocol-level metadata. MCP reserves prefixes containing modelcontextprotocol or mcp. Other meta keys allowed for custom use.
2.5 Standard JSON-RPC error codes
| Code | Meaning |
|---|---|
| -32700 | Parse error |
| -32600 | Invalid request |
| -32601 | Method not found |
| -32602 | Invalid params |
| -32603 | Internal error |
| -32000 to -32099 | Server-defined |
MCP-specific:
| Code | Meaning |
|---|---|
| -32002 | Resource not found |
3. Lifecycle
Three phases per modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle:
3.1 Initialization
// Client → server
{
jsonrpc: "2.0", id: 1, method: "initialize",
params: {
protocolVersion: "2025-06-18",
capabilities: {
roots: { listChanged: true },
sampling: {}
},
clientInfo: { name: "claude-code", version: "2.1.144" }
}
}
// Server → client
{
jsonrpc: "2.0", id: 1,
result: {
protocolVersion: "2025-06-18", // negotiated
capabilities: {
tools: { listChanged: true },
resources: { subscribe: true, listChanged: true },
prompts: { listChanged: true },
logging: {},
completions: {}
},
serverInfo: { name: "github-mcp", version: "1.2.0" },
instructions: "Server instructions visible to the LLM" // optional
}
}
// Client → server (after handshake complete)
{
jsonrpc: "2.0", method: "notifications/initialized"
}Server can refuse the connection by responding with an error.
3.2 Operation
Normal request/response/notification. Both sides may initiate.
3.3 Shutdown
- stdio: client closes input stream; server should exit cleanly
- HTTP: client closes the HTTP connection No explicit shutdown method.
4. Transports
4.1 stdio
Server is a subprocess; client reads/writes via stdin/stdout. Newline-delimited JSON-RPC.
- Simplest, no auth needed (process trust = parent process trust)
- Best for local tools (filesystem access, dev tools)
- One client per server process
- Cannot push notifications without polling
4.2 HTTP+SSE (deprecated)
Legacy: client POSTs requests to HTTP endpoint, receives responses via Server-Sent Events on a separate GET endpoint.
Deprecated as of 2024-11-05 spec revision. Use streamable-HTTP instead.
4.3 Streamable HTTP (current, 2025-03-26+)
Single HTTP endpoint. Client POSTs JSON-RPC requests; server may respond with:
- a regular JSON HTTP response (200), or
- an SSE stream (
Content-Type: text/event-stream) for streaming responses and server-initiated notifications
Bidirectional via the same endpoint. Stateful or stateless, server’s choice.
4.4 WebSocket (proposed)
Bidirectional persistent connection. Lower-overhead than streamable HTTP for high-throughput cases. Spec includes it under ws:// / wss:// URI scheme.
5. Server features
5.1 Tools (model-controlled)
Per modelcontextprotocol.io/specification/2025-06-18/server/tools. The model decides when to call them.
Capability declaration
{ "capabilities": { "tools": { "listChanged": true } } }tools/list
// Request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/list",
"params": { "cursor": "optional-cursor-value" } }
// Response
{ "jsonrpc": "2.0", "id": 1, "result": {
"tools": [{
"name": "get_weather",
"title": "Weather Information Provider",
"description": "Get current weather for a location",
"inputSchema": {
"type": "object",
"properties": { "location": { "type": "string" } },
"required": ["location"]
},
"outputSchema": { /* optional, for structured outputs */ },
"annotations": { /* readOnlyHint, destructiveHint, idempotentHint, openWorldHint, title */ }
}],
"nextCursor": "next-page-cursor"
}
}tools/call
// Request
{ "jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": { "name": "get_weather", "arguments": { "location": "NYC" } } }
// Response (success)
{ "jsonrpc": "2.0", "id": 2, "result": {
"content": [{ "type": "text", "text": "72°F, partly cloudy" }],
"isError": false
}
}
// Response (error)
{ "jsonrpc": "2.0", "id": 4, "result": {
"content": [{ "type": "text", "text": "API rate limit exceeded" }],
"isError": true
}
}Tool result content types
{ "type": "text", "text": "..." }{ "type": "image", "data": "<base64>", "mimeType": "image/png", "annotations": {...} }{ "type": "audio", "data": "<base64>", "mimeType": "audio/wav" }{ "type": "resource_link", "uri": "...", "name": "...", "mimeType": "..." }{ "type": "resource", "resource": { "uri": "...", "mimeType": "...", "text": "..." | "blob": "..." } }(embedded)structuredContent(top-level field): JSON object matching outputSchema
Tool annotations (untrusted, treat as hints)
readOnlyHint— tool doesn’t mutatedestructiveHint— tool may make destructive changesidempotentHint— safe to retryopenWorldHint— interacts with arbitrary external systemstitle— display name override
Security note: per spec, “Clients MUST consider tool annotations to be untrusted unless they come from trusted servers.”
tools/list_changed notification
Sent by server when tool list changes; client should re-list.
5.2 Resources (application-controlled)
Per modelcontextprotocol.io/specification/2025-06-18/server/resources. The application/host decides when to include them in context.
Capability
{ "capabilities": { "resources": { "subscribe": true, "listChanged": true } } }resources/list
{ "result": {
"resources": [{
"uri": "file:///project/src/main.rs",
"name": "main.rs",
"title": "Rust Software Application Main File",
"description": "Primary application entry point",
"mimeType": "text/x-rust",
"size": 1024
}],
"nextCursor": "..."
}}resources/read
{ "result": {
"contents": [{
"uri": "file:///project/src/main.rs",
"mimeType": "text/x-rust",
"text": "fn main() {...}"
}]
}}Binary content uses "blob": "<base64>" instead of "text".
resources/templates/list — parameterized resources via URI templates (RFC 6570)
{ "result": {
"resourceTemplates": [{
"uriTemplate": "file:///{path}",
"name": "Project Files",
"description": "Access files in the project directory",
"mimeType": "application/octet-stream"
}]
}}resources/subscribe + notifications/resources/updated
For watching specific resources.
Common URI schemes
https://— only when client can fetch directlyfile://— filesystem-like (need not be real filesystem)git://— Git VCS- Custom — RFC 3986-compliant
Annotations
audience:["user"],["assistant"],["user", "assistant"]priority: 0.0 to 1.0lastModified: ISO 8601
5.3 Prompts (user-controlled)
Per modelcontextprotocol.io/specification/2025-06-18/server/prompts. The user decides when to invoke them (typically as slash commands).
Capability
{ "capabilities": { "prompts": { "listChanged": true } } }prompts/list
{ "result": {
"prompts": [{
"name": "code_review",
"title": "Request Code Review",
"description": "Asks the LLM to analyze code quality",
"arguments": [{
"name": "code", "description": "Code to review", "required": true
}]
}]
}}prompts/get
// Request
{ "params": { "name": "code_review", "arguments": { "code": "def hello(): print('world')" } } }
// Response
{ "result": {
"description": "Code review prompt",
"messages": [{
"role": "user",
"content": { "type": "text", "text": "Please review this Python code:\ndef hello(): print('world')" }
}]
}}Prompts in Claude Code surface as /mcp__servername__promptname.
6. Client features
6.1 Roots
Client tells server which top-level directories / workspaces are in scope. Server uses these to constrain its operations.
// Client → server
{ "method": "roots/list_changed" } // notification
// Server → client (asking)
{ "method": "roots/list" }
// Client → server (response)
{ "result": { "roots": [{ "uri": "file:///workspace", "name": "Project" }] } }6.2 Sampling
Server asks client to invoke the LLM. Server-initiated AI calls.
// Server → client
{ "method": "sampling/createMessage", "params": {
"messages": [...],
"modelPreferences": { "hints": [{"name": "claude-sonnet"}] },
"systemPrompt": "...",
"maxTokens": 1024
}}Client returns the LLM response. Lets servers act semi-autonomously.
6.3 Elicitation
Server requests structured input from the user mid-task. Two modes:
- Form — server sends a schema; client renders a form
- URL — server sends a URL; client opens it (OAuth-style)
Used for credentials, approvals, additional context the agent doesn’t have.
7. Authorization (HTTP transports)
Per modelcontextprotocol.io/specification/2025-06-18/basic/authorization. stdio transports SHOULD NOT use this — they get credentials from environment.
7.1 OAuth 2.1 with PKCE (default)
- Server publishes RFC 8414 metadata at
/.well-known/oauth-authorization-server - Or RFC 9728 Protected Resource Metadata at
/.well-known/oauth-protected-resource - Client discovers, performs PKCE flow, stores token (e.g. system keychain on macOS)
- Tokens refreshable;
offline_accessscope requested if advertised
7.2 Dynamic Client Registration (RFC 7591)
Server may allow runtime client registration — no pre-configuration needed.
7.3 Pre-configured credentials
For servers that don’t support DCR (claude-code’s --client-id + --client-secret).
7.4 Client ID Metadata Document (CIMD)
Alternative discovery — server fetches client metadata from a URL the client publishes.
7.5 Custom auth
Clients and servers MAY negotiate custom strategies (mTLS, signed requests, etc.).
8. Utilities
8.1 Pagination
tools/list, resources/list, resources/templates/list, prompts/list all accept cursor and return nextCursor.
8.2 Cancellation
{ "method": "notifications/cancelled", "params": { "requestId": 42, "reason": "user_cancelled" } }8.3 Progress
{ "method": "notifications/progress", "params": {
"progressToken": "...",
"progress": 50,
"total": 100
}}8.4 Logging
Server → client, levels: debug, info, notice, warning, error, critical, alert, emergency.
{ "method": "notifications/message", "params": {
"level": "info", "logger": "github-api",
"data": "Fetched 50 issues"
}}8.5 Completion
For arg autocomplete:
{ "method": "completion/complete", "params": {
"ref": { "type": "ref/prompt", "name": "review" },
"argument": { "name": "language", "value": "py" }
}}
// → "python", "py3", ...9. SDK landscape
Official SDKs at github.com/modelcontextprotocol:
| Language | Package |
|---|---|
| Python | mcp (pip install mcp) — python-sdk |
| TypeScript | @modelcontextprotocol/sdk — typescript-sdk |
| Go | github.com/modelcontextprotocol/go-sdk |
| Rust | mcp-rust |
| Java | io.modelcontextprotocol:sdk |
| Kotlin | io.modelcontextprotocol:sdk-kotlin |
| C# / .NET | ModelContextProtocol |
| Swift | swift-mcp |
| Ruby | mcp |
| PHP | mcp-php |
Python server example
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather-server")
@mcp.tool()
def get_weather(location: str) -> str:
"""Get current weather for a location."""
return f"72°F, partly cloudy in {location}"
@mcp.resource("weather://current/{location}")
def weather_resource(location: str) -> str:
return f"Current weather data for {location}"
@mcp.prompt()
def review_weather(location: str) -> str:
return f"Review the current weather conditions in {location} and suggest activities."
if __name__ == "__main__":
mcp.run() # stdio by default; mcp.run(transport="streamable-http") for HTTPTypeScript server example
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new Server(
{ name: "weather-server", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler({ method: "tools/list" }, async () => ({
tools: [{
name: "get_weather",
description: "Get current weather",
inputSchema: {
type: "object",
properties: { location: { type: "string" } },
required: ["location"]
}
}]
}));
server.setRequestHandler({ method: "tools/call" }, async (req) => {
if (req.params.name === "get_weather") {
return { content: [{ type: "text", text: `72°F in ${req.params.arguments.location}` }] };
}
});
const transport = new StdioServerTransport();
await server.connect(transport);10. Anthropic’s MCP integration points
- Claude Code —
claude mcp add/.mcp.json(see claude-code-cli and the MCP section in CLI) - Claude Desktop / Web / iOS — MCP servers configured via
claude.ai/customize/connectors; flow through to Claude Code when logged in via subscription - Claude Agent SDK —
mcp_serversoption (see claude-agent-sdk) - Messages API directly — via the
mcp-client-2025-04-04/mcp-client-2025-11-20beta headers and MCP connector
11. Anthropic Directory of MCP servers
claude.ai/directory catalogs reviewed connectors. Same MCP infrastructure as direct configuration; add to Claude Code with claude mcp add.
Popular examples:
mcp.stripe.com— Stripemcp.notion.com/mcp— Notionmcp.asana.com/sse— Asana (SSE, legacy)mcp.sentry.dev/mcp— Sentryapi.githubcopilot.com/mcp— GitHubmcp.hubspot.com/anthropic— HubSpotmcp.paypal.com/mcp— PayPal@playwright/mcp@latest— Playwright (stdio)@bytebase/dbhub— PostgreSQL / MySQL / SQLite
12. MCP for Claude Code specifically — quick reference
Servers in Claude Code surface as tools named mcp__<server>__<tool> (per claude-code-cli).
Config in ~/.claude.json (user-scoped) or .mcp.json (project-scoped):
{
"mcpServers": {
"github": {
"type": "http",
"url": "https://api.githubcopilot.com/mcp/",
"headers": { "Authorization": "Bearer ${GITHUB_TOKEN}" }
},
"postgres": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@bytebase/dbhub", "--dsn", "${DB_URL}"],
"alwaysLoad": true, // skip tool-search deferral
"timeout": 600000 // ms
}
}
}Env-var expansion: ${VAR} or ${VAR:-default}. Expanded in command, args, env, url, headers.
Special env vars set by Claude Code in spawned MCP server processes:
CLAUDE_PROJECT_DIR— project root (same as hooks)CLAUDE_PLUGIN_ROOT/CLAUDE_PLUGIN_DATA— plugin-provided servers
Servers can call MCP roots/list to get the project directory.
The server name workspace is reserved.
13. Tool search
Claude Code defers MCP tool definitions out of context by default — only tool names load at session start. ToolSearch tool searches when needed.
Configure via ENABLE_TOOL_SEARCH:
- (unset) — defer all (fall back to load-upfront on Vertex AI or custom base URL)
true— defer all regardless of platformauto— load upfront if fits in 10 % of context, defer overflowauto:N— custom percentage (0-100)false— load all upfront
Exempt specific servers with alwaysLoad: true in config, or individual tools with _meta: {"anthropic/alwaysLoad": true}.
Requires tool_reference block support: Sonnet 4 / Opus 4 and later. Haiku doesn’t support.
14. Common gotchas
- stdio buffering: server must flush stdout after each JSON-RPC line, or Claude Code hangs.
- HTTP server returning non-JSON 2xx body: parsed as plain text context — usually intentional, but a bug if you meant to return JSON-RPC.
- Capability mismatch: client uses a feature server didn’t declare. Server should return
-32601 method not found. - OAuth redirect URI: Claude Code picks random port by default. Use
--callback-portto fix it. - Output too large: MCP tool outputs over 10,000 tokens trigger warning. Configure
MAX_MCP_OUTPUT_TOKENS(default 25,000) or addanthropic/maxResultSizeCharsannotation on tool (up to 500,000). workspaceserver name reserved — silently skipped.- Auto-reconnect: HTTP/SSE auto-reconnect with exponential backoff (5 attempts, 1s start, doubling). Stdio servers do NOT auto-reconnect.
- Initial connection retries: as of Claude Code v2.1.121, up to 3 retries on transient 5xx / connection-refused / timeout. Auth and 404 NOT retried.
- Plugin MCP servers: lifecycle automatic with plugin enable/disable; run
/reload-pluginsmid-session. - Claude.ai connectors only load when claude.ai subscription is active auth — env-var auth or
apiKeyHelperhides them.