Skip to main content
MCP

Advanced MCP protocol: transports and architecture

JSON-RPC 2.0 specification, stdio/SSE/HTTP transports, capabilities negotiation, security, and MCP debugging.

Beyond the basics

You know how to create an MCP, install tools, and use them in Claude Code. This page goes deeper: the underlying JSON-RPC protocol, the different available transports, capabilities negotiation, and advanced debugging techniques.

Who is this for?

This content is for developers who want to understand the protocol in depth, deploy remote MCPs (SSE, HTTP), or diagnose complex issues. If you're just getting started, begin with the TypeScript or Python tutorials.

JSON-RPC 2.0 specification

MCP is built on JSON-RPC 2.0, a lightweight protocol for communication via JSON messages. Each exchange follows a strict format: request, response, or notification.

Request format

{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get-weather",
"arguments": { "city": "Paris" }
}
}
  • jsonrpc: always "2.0"
  • id: unique identifier to correlate the response
  • method: the method being called
  • params: the call parameters

Response format

{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "Weather in Paris: 18°C, Cloudy"
}
]
}
}

Main MCP methods

MethodDirectionDescription
initializeClient to ServerInitializes the connection, exchanges capabilities
tools/listClient to ServerLists all tools exposed by the server
tools/callClient to ServerCalls a tool with arguments
resources/listClient to ServerLists all available resources
resources/readClient to ServerReads the content of a resource by URI
prompts/listClient to ServerLists available prompts
prompts/getClient to ServerRetrieves a prompt with its arguments
notifications/cancelledClient to ServerCancels an in-progress request

Notifications vs Requests

Notifications have no id field and don't expect a response. They're used for one-way events: resource changes, progress updates, cancellations.

Capabilities negotiation

At startup, Claude Code and the MCP Server exchange their capabilities. This is the first interaction after the connection is established.

Initialization request

{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {
"sampling": {}
},
"clientInfo": {
"name": "claude-code",
"version": "1.0.0"
}
}
}

Server response

{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {},
"resources": { "subscribe": true },
"prompts": {}
},
"serverInfo": {
"name": "mcp-weather",
"version": "1.0.0"
}
}
}

The client and server agree on the protocol version and announce what they support. A server that doesn't declare tools in its capabilities won't receive tools/call requests.

Available capabilities

CapabilitySideDescription
toolsServerThe server exposes tools
resourcesServerThe server exposes resources
resources.subscribeServerThe client can subscribe to resource changes
promptsServerThe server exposes prompts
samplingClientThe client supports sampling (see below)

Stdio transport: the default mode

The stdio (standard input/output) transport is the simplest and most common. Claude Code launches the MCP Server process and communicates via the process's stdin/stdout streams.

How it works

Claude Code                      MCP Server (child process)
     |                                    |
     |--- stdin: JSON-RPC request ------->|
     |                                    |--- processing
     |<--- stdout: JSON-RPC response -----|
     |                                    |
     |--- stdin: notification ----------->|

Each message is a JSON line terminated by \n. Server logs go through stderr to avoid interfering with the protocol.

Configuration

{
"mcpServers": {
"my-mcp": {
"command": "node",
"args": ["dist/index.js"],
"env": { "API_KEY": "..." }
}
}
}

Advantages and limitations

  • Simple: no networking, no ports, no server configuration
  • Secure: communication is local, no network attack surface
  • Limited: one process per connection, no sharing between multiple clients
  • Local: the MCP must run on the same machine as Claude Code

SSE transport: Server-Sent Events

The SSE transport lets you connect Claude Code to a remote MCP Server via HTTP. The client sends POST requests and receives responses via an SSE (Server-Sent Events) stream.

When to use it

  • The MCP Server runs on a remote server (cloud, VPS, internal network)
  • Multiple clients need to connect to the same MCP Server
  • You want an MCP Server that's always available, without restarting it each session

Configuration

{
"mcpServers": {
"remote-db": {
"url": "https://mcp.example.com/sse",
"headers": {
"Authorization": "Bearer your-token"
}
}
}
}

Security is mandatory

An SSE MCP is network-accessible. Protect it at all costs: token authentication, mandatory HTTPS, IP allowlisting if possible. An MCP exposed without protection is a critical security flaw.

Communication flow

Claude Code                           MCP Server (HTTP)
     |                                       |
     |--- GET /sse (establish SSE stream) -->|
     |<--- SSE: endpoint URL ----------------|
     |                                       |
     |--- POST /messages (request) --------->|
     |<--- SSE: JSON-RPC response -----------|

Streamable HTTP transport: the new standard

The Streamable HTTP transport is the successor to SSE for remote MCPs. It combines HTTP simplicity with bidirectional streaming.

Differences from SSE

CriterionSSEStreamable HTTP
DirectionUnidirectional (server to client)Bidirectional
ConnectionLong-lived (persistent SSE stream)Individual requests, streamed if needed
ResumptionManual reconnectionNative resumption support
ProxySome proxies cut SSE connectionsCompatible with all HTTP proxies

Configuration

{
"mcpServers": {
"remote-api": {
"url": "https://mcp.example.com/mcp",
"headers": {
"Authorization": "Bearer your-token"
}
}
}
}

Gradual adoption

Streamable HTTP transport is newer than SSE. Check that the MCP server you're targeting supports it. The official SDK (TypeScript and Python) supports it in recent versions.

Sampling: when the MCP asks the LLM

Sampling is a special capability: it lets the MCP Server ask Claude to complete a prompt. It's a reversed flow where the server temporarily becomes a "client" of the LLM.

Use cases

  • A translation tool that asks Claude to translate text
  • An analysis tool that asks Claude to summarize data
  • A generation tool that uses Claude to produce content

Communication flow

Claude Code        MCP Server               Claude (LLM)
     |                  |                         |
     |-- tools/call --->|                         |
     |                  |--- sampling/request ---->|
     |                  |<--- LLM response --------|
     |<-- result -------|                         |

Cost impact

Each sampling request consumes additional tokens. An MCP that uses sampling intensively can increase your costs. Monitor consumption with /cost in Claude Code.

Performance and latency

MCPs add latency to every interaction. Here are the main sources and how to optimize them.

Latency sources

SourceTypical latencyOptimization
Process startup (stdio)500ms-2sKeep the process alive
Tool discovery (tools/list)10-50msClient-side caching
Tool call (tools/call)Varies by toolAppropriate timeouts
External API call100ms-5sCache, retry, fallback
Network transport (SSE/HTTP)20-100msGeographically close server

Timeout management

Claude Code imposes a default timeout on MCP calls. If your tool does long-running operations:

// In your tool handler, send progress notifications
server.tool(
"long-operation",
"Operation that takes time",
{ input: z.string() },
async ({ input }, { sendProgress }) => {
await sendProgress(0, 100, "Starting...");
// ... long operation
await sendProgress(50, 100, "Processing...");
// ... continued
await sendProgress(100, 100, "Done");
return {
content: [{ type: "text" as const, text: "Operation result" }],
};
}
);

Advanced security

Sandboxing

Each MCP Server runs in its own process. But it has access to the host machine's filesystem and network. To limit risk:

  • Run MCPs in a Docker container with restricted volumes
  • Use tokens with minimal scopes (read-only when possible)
  • Apply network rules to limit outbound connections

Audit logs

To trace what an MCP does:

// Logging wrapper for all tool calls
server.tool("my-tool", "Description", { param: z.string() }, async ({ param }) => {
console.error(`[AUDIT] tool=my-tool param=${param} time=${new Date().toISOString()}`);
// ... tool logic
const result = "...";
console.error(`[AUDIT] tool=my-tool result_length=${result.length}`);
return { content: [{ type: "text" as const, text: result }] };
});

Logs on stderr don't interfere with the protocol and can be captured by your monitoring system.

Token rotation

For MCPs that use external API tokens:

  • Store tokens in environment variables, not in code
  • Set up regular rotation of tokens (every 90 days minimum)
  • Use a secret manager (Vault, AWS Secrets Manager) for production
  • Immediately revoke any potentially compromised token

Debugging: diagnosing problems

JSON-RPC inspection

The MCP inspector is your best debugging tool:

# Inspect a stdio server
npx @modelcontextprotocol/inspector node dist/index.js
# Inspect an SSE server
npx @modelcontextprotocol/inspector --url https://mcp.example.com/sse

The inspector displays every JSON-RPC message exchanged, letting you see exactly what the server receives and returns.

Verbose logging in Claude Code

# Launch Claude Code with detailed MCP logs
claude --verbose

The --verbose flag displays JSON-RPC exchanges with each MCP in the terminal.

Check MCP health

# List configured MCPs and their status
claude mcp list

If an MCP shows as "disconnected" or "error":

Verify the command works

Run the MCP command directly in your terminal to see errors:

# Example for a Node.js MCP
node /path/to/mcp/dist/index.js

If it crashes, the error message will tell you the problem (missing dependency, syntax error, etc.).

Check environment variables

Missing tokens are the most frequent cause. Verify that all env variables in the .mcp.json are correct.

Test with the inspector

npx @modelcontextprotocol/inspector node /path/to/mcp/dist/index.js

If the inspector works but Claude Code doesn't, the problem is in the Claude configuration (path, arguments, permissions).

Common debugging errors

SymptomLikely causeDiagnosis
MCP "disconnected" at startupProcess crashingRun the command manually
Tools not listedError in initializeCheck with the MCP inspector
Empty response or timeoutHandler that returns nothingAdd stderr logs
parse error JSON-RPCconsole.log() in codeReplace with console.error()
Expired tokenStale credentialsRegenerate the token and update env

Next steps