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

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:

  1. Base protocol — JSON-RPC 2.0 message types
  2. Lifecycle — initialize → operate → shutdown
  3. Authorization — OAuth 2.0 + alternatives for HTTP transports
  4. Server features — tools, resources, prompts
  5. 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

CodeMeaning
-32700Parse error
-32600Invalid request
-32601Method not found
-32602Invalid params
-32603Internal error
-32000 to -32099Server-defined

MCP-specific:

CodeMeaning
-32002Resource 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 mutate
  • destructiveHint — tool may make destructive changes
  • idempotentHint — safe to retry
  • openWorldHint — interacts with arbitrary external systems
  • title — 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 directly
  • file:// — 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.0
  • lastModified: 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_access scope 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:

LanguagePackage
Pythonmcp (pip install mcp) — python-sdk
TypeScript@modelcontextprotocol/sdktypescript-sdk
Gogithub.com/modelcontextprotocol/go-sdk
Rustmcp-rust
Javaio.modelcontextprotocol:sdk
Kotlinio.modelcontextprotocol:sdk-kotlin
C# / .NETModelContextProtocol
Swiftswift-mcp
Rubymcp
PHPmcp-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 HTTP

TypeScript 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 Codeclaude 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 SDKmcp_servers option (see claude-agent-sdk)
  • Messages API directly — via the mcp-client-2025-04-04 / mcp-client-2025-11-20 beta 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 — Stripe
  • mcp.notion.com/mcp — Notion
  • mcp.asana.com/sse — Asana (SSE, legacy)
  • mcp.sentry.dev/mcp — Sentry
  • api.githubcopilot.com/mcp — GitHub
  • mcp.hubspot.com/anthropic — HubSpot
  • mcp.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.

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 platform
  • auto — load upfront if fits in 10 % of context, defer overflow
  • auto: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

  1. stdio buffering: server must flush stdout after each JSON-RPC line, or Claude Code hangs.
  2. HTTP server returning non-JSON 2xx body: parsed as plain text context — usually intentional, but a bug if you meant to return JSON-RPC.
  3. Capability mismatch: client uses a feature server didn’t declare. Server should return -32601 method not found.
  4. OAuth redirect URI: Claude Code picks random port by default. Use --callback-port to fix it.
  5. Output too large: MCP tool outputs over 10,000 tokens trigger warning. Configure MAX_MCP_OUTPUT_TOKENS (default 25,000) or add anthropic/maxResultSizeChars annotation on tool (up to 500,000).
  6. workspace server name reserved — silently skipped.
  7. Auto-reconnect: HTTP/SSE auto-reconnect with exponential backoff (5 attempts, 1s start, doubling). Stdio servers do NOT auto-reconnect.
  8. 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.
  9. Plugin MCP servers: lifecycle automatic with plugin enable/disable; run /reload-plugins mid-session.
  10. Claude.ai connectors only load when claude.ai subscription is active auth — env-var auth or apiKeyHelper hides them.

15. Further reading