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.

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

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

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

MCP Elicitation: requesting input mid-task

Elicitation is a feature introduced in 2026 that allows an MCP Server to request structured input from the user during execution, via an interactive dialog. A tool no longer needs to anticipate everything in its input parameters.

Use cases

  • A deployment MCP that asks for confirmation before going to production
  • An email-sending MCP that asks the user to choose from several templates
  • A database migration MCP that asks the user to validate transformed data before applying it

Communication flow

Claude Code                MCP Server
     |                         |
     |--- tools/call --------->|
     |                         |--- (needs a user decision)
     |<-- elicitation/request--|
     |                         |
     | [dialog in the terminal]
     | User provides input
     |                         |
     |--- elicitation/response >|
     |                         |--- (continues execution)
     |<-- final result ---------|

Implementing Elicitation in your MCP

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { ElicitRequest } from "@modelcontextprotocol/sdk/types.js";
const server = new Server(
{ name: "deploy-mcp", version: "1.0.0" },
{ capabilities: { tools: {}, elicitation: {} } } // Declare the capability
);
server.tool(
"deploy-to-production",
"Deploy the application to production",
{
version: z.string(),
environment: z.enum(["staging", "production"]),
},
async ({ version, environment }, { elicit }) => {
// Ask the user for confirmation before critical deployment
if (environment === "production") {
const confirmation = await elicit({
message: `Confirm deployment of version ${version} to production?`,
requestedSchema: {
type: "object",
properties: {
confirmed: {
type: "boolean",
title: "Confirm",
description: "Type true to confirm the deployment",
},
rollbackPlan: {
type: "string",
title: "Rollback plan",
description: "Describe your rollback plan in case of issues",
},
},
required: ["confirmed"],
},
});
if (!confirmation.confirmed) {
return {
content: [{ type: "text", text: "Deployment cancelled by user." }],
};
}
}
// Proceed with deployment
const result = await doDeployment(version, environment);
return {
content: [{ type: "text", text: `Deployed: ${result.url}` }],
};
}
);

What the user sees in Claude Code

When an MCP triggers an Elicitation, Claude Code shows an interactive dialog in the terminal:

╔══════════════════════════════════════════════════╗
║  MCP deploy-mcp — Input required                 ║
╠══════════════════════════════════════════════════╣
║                                                  ║
║  Confirm deployment of version v2.3.1            ║
║  to production?                                  ║
║                                                  ║
║  Confirm [true/false]: _                         ║
║  Rollback plan (optional): _                     ║
║                                                  ║
╚══════════════════════════════════════════════════╝

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

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

1

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

2

Check environment variables

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

3

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