Quick Answer
When an MCP client reports "failed to reconnect," work through this path first:
- Check whether the server process is still running (stdio) or responding to HTTP (remote).
- Read the disconnect reason — the root cause is almost always in the disconnect event, not the reconnect attempt.
- Determine whether the server is stateful — if it is, a reconnect that lands on a different instance or after a restart will always fail until the session is re-established from scratch.
- Check authentication — expired OAuth tokens silently convert working connections into 401 failures on reconnect.
- Look for retry storm signatures — rapid repeated reconnect attempts in logs indicate missing backoff, which can make recovery harder.
Reconnect failures are almost never about the reconnect mechanism itself. They are about a changed server state that the client does not know about.
MCP Failed to Reconnect: 60-Second Checklist
Work through these in order. Stop when you find the failure.
- Server process alive? — For stdio, check
ps aux | grep <server-name>. For HTTP,curl -v <server-url>/health. - Original disconnect logged? — Find the disconnect event in client or server logs before looking at reconnect errors.
- Server restarted or redeployed recently? — Any restart invalidates in-memory session state.
- Session ID still valid? — Check server logs for session lookup results on the reconnect attempt.
- Auth token still valid? — Check token expiry time. Run the OAuth token endpoint manually if needed.
- Reconnect hitting the same instance? — If behind a load balancer, verify session affinity (sticky sessions) is configured.
- Reconnect timing out at the proxy? — Check Nginx/ALB idle timeout configuration.
- Client re-running MCP initialization? — Verify the
initializehandshake fires after transport reconnect, not just the transport layer. - Tools/list called after reconnect? — Absence of tool discovery after reconnect is a separate step from the transport reconnect.
- Exponential backoff active? — Confirm the client is not hammering the server every 100ms.
Jump to the Right Fix
| Symptom | Likely Cause | Start Here |
|---|---|---|
| Server process not found after restart | stdio process crashed or binary path changed | Local stdio Reconnect Failures |
| HTTP 401 on reconnect | Expired OAuth token, no refresh | Authentication and OAuth Expiry |
| HTTP 403 on reconnect | Token still valid but scope changed or revoked | Authentication and OAuth Expiry |
| HTTP 404 on reconnect | Session expired, endpoint moved, or deployment replaced session | Streamable HTTP Session Troubleshooting |
| HTTP 429 on reconnect | Retry storm hitting rate limits | Reconnect Loops and Retry Storms |
| HTTP 5xx on reconnect | Server crashed or unhealthy | Remote MCP Reconnect Failures |
| Reconnect succeeds but no tools | MCP initialization skipped after transport reconnect | Missing Tools After Reconnect |
| Reconnect succeeds but initialize fails | Server state reset, capability mismatch | Initialization Failures After Reconnect |
| Fresh connection works, reconnect fails | Stateful server lost session | Streamable HTTP Session Troubleshooting |
| Reconnect worked before, broken after deploy | Deployment invalidated sessions | Server Restart and Deployment Failures |
| Reconnect loops endlessly | No backoff, permanent underlying error | Reconnect Loops and Retry Storms |
| Different behavior behind load balancer | Session affinity missing | Load Balancer and Multi-Instance Problems |
Want to analyze your API security?
Import your OpenAPI spec and generate a Security Report automatically.
Initial Connection vs. Disconnect vs. Reconnect Failure
These three failure types look similar in client logs but have completely different causes.
Initial connection failure — the client has never successfully connected. The server may be unreachable, the configuration is wrong, or authentication was never established. See MCP failed to connect for this scenario.
Disconnect — a working connection dropped. The transport closed cleanly or was interrupted. The disconnect itself is often not an error — what matters is why it happened.
Reconnect failure — the client attempts to restore the connection after a disconnect and cannot. This is the scenario this guide covers. The root cause is almost always in the state that existed at disconnect time, not in the reconnect logic itself.
This distinction matters because reconnect failures require diagnosing what changed between the disconnect and the reconnect attempt, not just what is broken now.
Finding the Original Disconnect Cause
Every reconnect failure investigation should start here, not at the reconnect error.
Where to Look
Client-side logs — MCP clients like Claude Desktop, Cursor, and custom SDK clients log disconnect events with reason codes. Find the log line that shows the connection closing, not the one showing reconnect failure.
Server-side logs — The server typically logs why it closed the connection: idle timeout, authentication failure, process signal, or unhandled exception.
Transport-level signals:
- stdio: Did the child process exit? What was the exit code? A non-zero exit code means a crash. Exit code 0 means the process completed normally and was not designed to stay alive.
- Streamable HTTP: What was the final HTTP status on the last request before disconnect? A 408 is a server-side timeout. A 503 is a server going away.
Correlating Disconnect to Reconnect Failure
Disconnect reason → What it means for reconnect
─────────────────────────────────────────────────────────────
Process exited (exit code != 0) → Server crashed; may not restart cleanly
Idle timeout (proxy/server) → Session may have been GC'd on server side
Network interruption → Session may still exist if server is stateful
Server sent close frame → Server intentionally closed; session likely invalid
Auth token expired → New token required before any reconnect attempt
Server restarted → All in-memory session state is gone
If you find the disconnect was caused by an expired token or a server restart, do not attempt a transport reconnect before fixing the underlying condition. The reconnect will fail for the same reason.
Local stdio Reconnect Failures
For stdio-based MCP servers, "reconnect" means spawning a new child process. There is no persistent session at the transport layer — each spawn is a fresh process.
The stdio Reconnect Model
When a stdio connection drops, the client kills the existing process (if any) and spawns a new one using the configured command. This is inherently a fresh start — there is no session resume, no handshake continuation, and no state transfer between the old and new process.
Reconnect failure in stdio almost always means the process is failing to start.
Diagnosing a Failed stdio Restart
Step 1: Run the server command manually
Copy the exact command from your MCP configuration and run it in a terminal:
# Example for a Node.js MCP server
node /path/to/server/index.js
# Example for a Python MCP server
python -m my_mcp_server
# Example for a binary
/usr/local/bin/my-mcp-server
If the process exits immediately, you have a startup crash. Read stderr output.
Step 2: Check the binary path
After an update, the binary path may have changed. Verify:
which my-mcp-server
ls -la /path/to/server
Step 3: Check environment variables
Many MCP server crashes at startup are missing API keys or configuration:
# Run with the same environment your client would use
MY_API_KEY=... node /path/to/server/index.js
Step 4: Check Node.js / Python version compatibility
If a server update requires a newer runtime:
node --version
python --version
Fixing stdio Reconnect Failures
| Failure | Fix |
|---|---|
ENOENT: no such file or directory | Update the binary path in client config |
Error: Cannot find module | Run npm install in the server directory |
Missing required environment variable | Add the variable to client MCP config env block |
| Process exits with code 1 immediately | Check server logs for unhandled exception at startup |
| Permission denied | chmod +x the server binary |
After fixing: Restart your MCP client (Claude Desktop, Cursor, etc.) to trigger a fresh process spawn. Do not just reload the connection — reload the full client if it caches the process state.
Remote MCP Reconnect Failures
For HTTP-based MCP servers, reconnect failures surface as HTTP errors. Each error has a distinct meaning.
HTTP Status Code Diagnostic Table
| Status | Meaning in Reconnect Context | Fix |
|---|---|---|
| 401 Unauthorized | Auth token expired or missing | Refresh or re-acquire the token |
| 403 Forbidden | Token valid but permission revoked or scope changed | Re-authorize with correct scopes |
| 404 Not Found | Session endpoint gone, URL changed, or session expired | Attempt fresh initialization; verify endpoint URL |
| 408 Request Timeout | Server timed out waiting for client | Reduce latency, check network path |
| 429 Too Many Requests | Retry storm hitting rate limit | Implement exponential backoff with jitter |
| 502 Bad Gateway | Upstream server down, proxy can't reach it | Check server health, restart if needed |
| 503 Service Unavailable | Server intentionally unavailable (deploy, maintenance) | Wait and retry with backoff |
| 504 Gateway Timeout | Upstream server too slow to respond | Check server performance, connection pool |
Verifying Server Health
# Basic health check
curl -v https://your-mcp-server.example.com/health
# Test the MCP endpoint directly
curl -v -X POST https://your-mcp-server.example.com/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}'
If this succeeds but your client still fails to reconnect, the problem is in the client's reconnect logic, not the server.
Streamable HTTP Session Troubleshooting
Streamable HTTP is the current recommended transport for remote MCP servers. Understanding its session semantics is essential for diagnosing reconnect failures.
How Streamable HTTP Sessions Work
In the Streamable HTTP transport, the server may (but is not required to) assign a session ID after the initialize request. If a session ID is assigned:
- The client includes it in subsequent requests via the
Mcp-Session-Idheader. - The server uses it to correlate requests to a logical session.
- Session state may be stored in memory on the server.
Critical: Not all Streamable HTTP servers are stateful. A stateless server that ignores the session ID will happily accept any reconnect as a fresh initialization. A stateful server that stores session data in memory will reject reconnects to lost sessions.
Common Session-Related Reconnect Failures
Stale session ID
The client reconnects using a session ID from the previous connection, but that session no longer exists on the server.
Symptom: HTTP 404 on reconnect requests that include Mcp-Session-Id.
Verification:
# Check server logs for a line like:
# "Session not found: sess_abc123"
# or
# "Unknown session ID"
Fix: The client should detect 404 responses to session-bearing requests and fall back to a fresh initialize without a session ID. If your client does not do this automatically, configure it to treat 404 as a session expiry and re-initialize.
Session ID format changed after server update
If a server update changes how session IDs are generated or validated, old session IDs become immediately invalid.
Fix: Treat any server update as a forced re-initialization. Clear cached session IDs on client restart.
In-memory session GC during idle
Servers that store session state in memory often garbage-collect idle sessions. A client that reconnects after a long idle period may find its session has been cleaned up.
Fix:
- Configure longer session TTL on the server if you control it.
- Design clients to handle 404 on session requests as a signal to re-initialize.
- Use persistent session storage (Redis, database) if sessions must survive server restarts.
Session Verification Flow
Client sends reconnect request with Mcp-Session-Id
│
▼
Server receives request
│
├─ Session found → Resume, return 200
│
└─ Session not found → Return 404
│
▼
Client should detect 404
│
├─ Drop session ID
├─ Send fresh initialize (no Mcp-Session-Id)
└─ Receive new session ID → Continue
Server Restart and Deployment Failures
A server restart is the most common cause of reconnect failures in production. It invalidates everything: in-memory session state, active transport connections, and any server-side context accumulated during the session.
Why Restarts Break Reconnects
- In-memory session state is gone. Any session ID the client holds is now invalid.
- Active SSE streams are closed. Clients connected via streaming will get a connection reset.
- Process environment may have changed. New deployments may use different configuration.
Detecting a Restart-Caused Reconnect Failure
Look for these patterns in server logs:
# Server log: restart happened
[2025-01-28T10:00:00Z] Server starting up
[2025-01-28T10:00:01Z] Listening on :3000
# Client log: reconnect attempt after restart
[2025-01-28T10:00:05Z] Reconnecting with session: sess_abc123
[2025-01-28T10:00:05Z] 404 Not Found — session not found
[2025-01-28T10:00:05Z] Reconnect failed
The timestamp gap between server restart and client reconnect attempt confirms the session predates the restart.
Blue-Green and Rolling Deployments
Rolling deployments are particularly dangerous for stateful MCP servers because:
- Old instances are gradually replaced with new ones.
- In-flight sessions may be on instances that get shut down mid-session.
- The client may reconnect to a new instance that has no knowledge of its session.
Fix options:
- Drain connections before terminating instances — configure your orchestrator (Kubernetes, ECS) to send
SIGTERM, wait for in-flight requests to complete, then terminate. - Externalize session state — store session data in Redis or a database so any instance can serve any session.
- Design for stateless reconnect — treat every reconnect as a fresh initialization. The server generates a new session ID and the client rebuilds state.
Load Balancer and Multi-Instance Problems
This is one of the most subtle categories of MCP reconnect failure because it works correctly in development (single instance) and fails unpredictably in production.
The Problem
Client
│
▼
Load Balancer
│
├──→ Instance A ← Session sess_abc123 stored here
│
└──→ Instance B ← No session sess_abc123
Initial connection routed to Instance A creates session sess_abc123. After disconnect, the client reconnects. The load balancer routes the reconnect to Instance B. Instance B has no record of sess_abc123. Reconnect fails with 404.
Verifying Load Balancer Routing
# Check which instance responds to successive requests
# Many servers expose instance ID in headers or response body
curl -v https://your-mcp-server.example.com/health | grep -i 'x-instance\|server-id'
# Or add instance identification to your server's health endpoint
# and call it multiple times to see if you get different instances
for i in {1..5}; do curl -s https://your-mcp-server.example.com/health; done
Fix 1: Sticky Sessions (Session Affinity)
Configure your load balancer to route requests with the same session identifier to the same backend instance.
Nginx upstream:
upstream mcp_backend {
ip_hash; # or use sticky cookie module
server instance1:3000;
server instance2:3000;
}
AWS ALB: Enable "Stickiness" on the target group with a duration that covers your longest expected session.
Kubernetes Ingress (nginx):
annotations:
nginx.ingress.kubernetes.io/affinity: "cookie"
nginx.ingress.kubernetes.io/session-cookie-name: "mcp-route"
nginx.ingress.kubernetes.io/session-cookie-expires: "172800"
Fix 2: Shared External Session Store
If sticky sessions are not viable (e.g., active-active failover requirements), move session state out of process memory:
import { createClient } from 'redis';
// Instead of storing session state in a Map:
// const sessions = new Map<string, SessionState>();
// Store in Redis with TTL:
const redis = createClient({ url: process.env.REDIS_URL });
async function getSession(sessionId: string): Promise<SessionState | null> {
const data = await redis.get(`mcp:session:${sessionId}`);
return data ? JSON.parse(data) : null;
}
async function setSession(sessionId: string, state: SessionState): Promise<void> {
await redis.setEx(
`mcp:session:${sessionId}`,
3600, // 1 hour TTL
JSON.stringify(state)
);
}
Fix 3: Stateless Server Design
If your server does not actually need session state between requests, remove session tracking entirely. Each request is self-contained. Reconnects always succeed because there is nothing to resume — every connection is treated as new.
Authentication and OAuth Expiry
Expired credentials are the second most common reconnect failure cause. OAuth access tokens typically expire in 1 hour, which means any MCP session lasting longer than the token lifetime will fail to reconnect if the token is not refreshed.
Identifying Token Expiry
Symptom: HTTP 401 on reconnect
Server log: "Token expired" or "Invalid token" or "JWT expired"
Client log: "Authentication failed" or "Unauthorized"
The Token Expiry Timeline Problem
T+0:00 Initial connection — token valid, expires at T+1:00
T+0:30 Session active — token still valid
T+0:55 Connection drops (network issue)
T+0:56 Client attempts reconnect — token expires in 4 minutes, reconnect may succeed
T+1:05 Client retries reconnect — token expired 5 minutes ago — reconnect fails with 401
If your client retries reconnects over a period that crosses the token expiry boundary, early retries may succeed while later retries fail. This produces inconsistent reconnect behavior that looks random but is actually deterministic.
Implementing Token Refresh Before Reconnect
async function reconnectWithTokenRefresh(client: MCPClient): Promise<void> {
const token = await getStoredToken();
// Refresh if token expires within 5 minutes
const REFRESH_BUFFER_MS = 5 * 60 * 1000;
if (Date.now() + REFRESH_BUFFER_MS >= token.expiresAt) {
try {
const refreshed = await refreshOAuthToken(token.refreshToken);
await storeToken(refreshed);
client.setAuthToken(refreshed.accessToken);
} catch (refreshError) {
// Refresh token itself may be expired
// Trigger re-authorization flow
await triggerReauthorization();
return;
}
}
await client.reconnect();
}
HTTP 403 vs 401
- 401 means the token is invalid or expired. Fix: get a new token.
- 403 means the token is valid but lacks permission. This can happen if scopes were revoked between the initial connection and the reconnect, or if the resource now requires elevated permissions. Fix: re-authorize with the required scopes.
Do not treat 403 as a transient error. It will not resolve with retries.
Reverse Proxy and Idle Timeout Problems
If your MCP server sits behind Nginx, HAProxy, AWS ALB, or a similar proxy, idle timeout configuration is a common silent killer of long-lived connections.
How It Happens
Client ←——————→ Nginx ←——————→ MCP Server
↑
proxy_read_timeout = 60s
If no data flows between client and server for 60 seconds, Nginx closes the connection. The client may not realize the connection was closed until it tries to send a message — at which point it attempts to reconnect. If the server treated the Nginx-side close as a session termination, the reconnect fails.
Nginx Configuration Fix
server {
location /mcp {
proxy_pass http://mcp_backend;
# Increase timeouts for long-lived MCP sessions
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 10s;
# Keep the upstream connection alive
proxy_http_version 1.1;
proxy_set_header Connection "";
# Forward session header
proxy_set_header Mcp-Session-Id $http_mcp_session_id;
}
}
AWS ALB Idle Timeout
AWS ALB has a default idle timeout of 60 seconds. For MCP servers with potentially long-lived sessions:
- Go to EC2 → Load Balancers → Select your ALB
- Attributes → Edit
- Set Idle timeout to match your session requirements (max 4000 seconds)
Application-Level Keepalive
Rather than relying on proxy timeout configuration (which you may not control), implement keepalive pings from the client side:
class MCPClientWithKeepalive {
private keepaliveInterval: NodeJS.Timer | null = null;
startKeepalive(intervalMs: number = 30000): void {
this.keepaliveInterval = setInterval(async () => {
try {
// Send a lightweight ping that the server can respond to
await this.client.ping();
} catch (error) {
// Connection dropped — trigger reconnect
this.handleDisconnect();
}
}, intervalMs);
}
stopKeepalive(): void {
if (this.keepaliveInterval) {
clearInterval(this.keepaliveInterval);
this.keepaliveInterval = null;
}
}
}
Reconnect Loops and Retry Storms
When an MCP client retries reconnects without backoff, a permanent failure becomes a thundering herd problem. The client hammers the server with requests it cannot handle, which can delay recovery or trigger rate limiting.
Identifying a Retry Storm
# Server logs showing a storm:
[10:00:00.001] POST /mcp — 503
[10:00:00.102] POST /mcp — 503
[10:00:00.203] POST /mcp — 503
[10:00:00.304] POST /mcp — 503
# 100ms intervals = no backoff
HTTP 429 responses from the server are the clearest signal. But some servers fail silently under load without returning 429.
Exponential Backoff with Jitter
MCP does not specify a reconnect retry policy. This is an application-level responsibility. Implement exponential backoff with full jitter:
interface RetryConfig {
maxAttempts: number;
baseDelayMs: number;
maxDelayMs: number;
jitterFactor: number; // 0–1
}
async function reconnectWithBackoff(
connect: () => Promise<void>,
config: RetryConfig
): Promise<void> {
let attempt = 0;
while (attempt < config.maxAttempts) {
try {
await connect();
return; // Success
} catch (error) {
attempt++;
// Do not retry on permanent errors
if (isPermanentError(error)) {
throw error; // 403, auth revoked, etc.
}
if (attempt >= config.maxAttempts) {
throw new Error(`Failed to reconnect after ${config.maxAttempts} attempts: ${error}`);
}
// Exponential backoff with full jitter
const exponentialDelay = config.baseDelayMs * Math.pow(2, attempt - 1);
const cappedDelay = Math.min(exponentialDelay, config.maxDelayMs);
const jitter = Math.random() * config.jitterFactor * cappedDelay;
const delay = cappedDelay + jitter;
console.log(`Reconnect attempt ${attempt} failed. Retrying in ${Math.round(delay)}ms`);
await sleep(delay);
}
}
}
function isPermanentError(error: unknown): boolean {
if (error instanceof HTTPError) {
// Do not retry client errors except 429
return error.status >= 400 && error.status < 500 && error.status !== 429;
}
return false;
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
Reconnect Storm Prevention Checklist
- Maximum retry count configured (not infinite)
- Base delay >= 1 second
- Exponential growth per attempt
- Jitter applied to prevent synchronized retries from multiple clients
- Permanent errors (403, 4xx non-429) do NOT trigger retry
- User is notified after max retries, not silently looping
For more on error handling patterns, see our guide on MCP error -32000 connection closed which covers related transport error semantics.
Initialization Failures After Reconnect
The transport reconnected. The TCP/HTTP connection is alive. But the MCP initialize handshake fails. This is a distinct failure from transport reconnection.
Why This Happens
- The server restarted and now has different capabilities than the client expects.
- The server version changed and the protocol version is no longer compatible.
- The server is returning an error during
initializebecause it requires a clean state the client is not providing. - The client is sending a cached
initializerequest that references a previous session context.
Verifying the Initialize Failure
# Send a fresh initialize and check the response
curl -X POST https://your-mcp-server.example.com/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_VALID_TOKEN" \
-d '{
"jsonrpc": "2.0",
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {
"roots": { "listChanged": true },
"sampling": {}
},
"clientInfo": {
"name": "debug-client",
"version": "1.0.0"
}
},
"id": 1
}'
A successful response returns serverInfo and capabilities. An error response will have a code and message explaining the failure.
Protocol Version Mismatch
If the server responds with a protocol version incompatibility error, check:
{
"jsonrpc": "2.0",
"error": {
"code": -32600,
"message": "Unsupported protocol version: 2024-11-05"
},
"id": 1
}
This means the server updated to a newer protocol version. Update your client SDK to match.
Missing Tools After Reconnect
Your client reconnected, the initialize handshake completed, but the tools list is empty or incomplete. This is the most common post-reconnect confusion.
Why Tools Disappear
Tool discovery in MCP is not automatic after reconnect. It requires an explicit tools/list call. If the client's reconnect logic restores the transport connection and runs initialize but does not re-run tools/list, the local tool registry will be empty.
The Complete Reconnect Sequence
1. Transport reconnect (TCP/HTTP connection established)
↓
2. MCP initialize request/response (capabilities negotiated)
↓
3. initialized notification sent by client
↓
4. tools/list called (tools discovered)
↓
5. resources/list called (if applicable)
↓
6. prompts/list called (if applicable)
↓
7. Session fully restored — ready for use
Skipping step 4 is the most common cause of missing tools after reconnect. Verify your client's reconnect handler calls tools/list after initialized.
Server-Side Tool Changes During Reconnect
If the server was updated between the disconnect and the reconnect, the available tools may have changed. This is expected behavior, not a bug. The tools/list response after reconnect is the source of truth.
If a tool the user was relying on is no longer in tools/list after reconnect, the tool was removed from the server — not lost due to a reconnect failure.
MCP Inspector Testing
MCP Inspector is the fastest way to isolate whether a reconnect failure is a server issue or a client issue.
Reconnect Testing Procedure with MCP Inspector
# Install MCP Inspector if needed
npx @modelcontextprotocol/inspector
# Or for a specific server
npx @modelcontextprotocol/inspector node /path/to/server/index.js
Testing reconnect behavior:
- Connect to your server in Inspector.
- Verify tools list is populated.
- Close the connection manually (Disconnect button).
- Wait 5 seconds.
- Reconnect.
- Observe: Does
initializesucceed? Doestools/listreturn results? - Check Inspector's network panel for the exact HTTP requests and responses during reconnect.
If Inspector reconnects successfully but your production client does not, the problem is in your client's reconnect logic, not the server.
If Inspector also fails to reconnect, the problem is server-side. You can also use the MCPForge server verification tool to run a conformance check against your server from an independent client.
Regression Troubleshooting: "It Used to Work"
When reconnect worked before and now fails after a change, narrow down what changed.
Regression Isolation Checklist
- Server updated? — Check Git history for changes to session handling, authentication middleware, or startup sequence.
- Client updated? — MCP client updates (Claude Desktop, Cursor, SDK version) may change reconnect behavior.
- Infrastructure changed? — New load balancer, changed proxy timeout, added WAF, modified TLS configuration.
- Auth provider changed? — Token expiry time shortened, scopes changed, new required claims.
- Configuration changed? — Environment variables, feature flags, connection pool settings.
Binary Search for the Regression
If you have Git history:
# Use git bisect to find when reconnect broke
git bisect start
git bisect bad HEAD
git bisect good <last-known-working-commit>
# Test each version
# git bisect good / git bisect bad
# Until git identifies the first bad commit
Minimal Reproduction Procedure
Before filing a bug report, create the smallest possible reproduction.
// minimal-reconnect-test.ts
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
async function testReconnect() {
const serverUrl = new URL('http://localhost:3000/mcp');
// Step 1: Initial connection
const transport1 = new StreamableHTTPClientTransport(serverUrl);
const client = new Client(
{ name: 'reconnect-test', version: '1.0.0' },
{ capabilities: {} }
);
console.log('Connecting...');
await client.connect(transport1);
console.log('Connected. Tools:', await client.listTools());
// Step 2: Simulate disconnect
console.log('Disconnecting...');
await client.close();
// Step 3: Wait (simulate idle period or network interruption)
await new Promise(r => setTimeout(r, 2000));
// Step 4: Reconnect
console.log('Reconnecting...');
const transport2 = new StreamableHTTPClientTransport(serverUrl);
try {
await client.connect(transport2);
const tools = await client.listTools();
console.log('Reconnected. Tools:', tools);
console.log('SUCCESS: Reconnect worked correctly');
} catch (error) {
console.error('FAILURE: Reconnect failed:', error);
process.exit(1);
} finally {
await client.close();
}
}
testReconnect();
Run this against your server. If it fails, you have a reproducible case you can debug or share.
Bug Report Checklist
If you need to report a reconnect failure to an SDK maintainer or server author, include:
- MCP SDK version (
npm list @modelcontextprotocol/sdkorpip show mcp) - Client name and version (Claude Desktop version, Cursor version, custom client)
- Transport type (stdio, Streamable HTTP, SSE)
- Server type (local process, remote HTTP, which framework)
- Exact error message from client logs
- Server logs from the moment of disconnect through the reconnect attempt
- HTTP traffic — request/response headers and bodies if HTTP transport (redact tokens)
- Reproduction steps — minimal script from the section above
- What changed before the regression appeared
- What works — does MCP Inspector reconnect? Does a fresh connection work?
Prevention Best Practices
Reconnect failures are largely preventable. These practices reduce their frequency and impact in production.
Server Design
- Externalize session state — Never store session data only in process memory if you run multiple instances.
- Handle
SIGTERMgracefully — Drain connections before shutdown; do not kill active sessions. - Return meaningful errors on session miss — A clear 404 with
{"error": "session_not_found"}is far easier to handle than a generic 500. - Implement session TTL — Set a reasonable expiry and communicate it to clients. Infinite session TTL is a memory leak.
Client Design
- Implement full reconnect sequence — Transport → initialize → initialized → tools/list, not just transport reconnect.
- Refresh tokens before expiry — Check token expiry before every reconnect attempt.
- Use exponential backoff with jitter — Never retry without delay.
- Treat permanent errors differently from transient ones — 403 should stop retrying immediately.
- Detect and handle session invalidation — 404 on a session-bearing request should trigger fresh initialization.
Infrastructure
- Configure proxy timeouts appropriately — Match idle timeouts to your session lifetime.
- Use sticky sessions or shared state — Choose based on your failover requirements.
- Monitor reconnect rate — A spike in reconnects indicates an upstream problem worth alerting on.
- Test reconnects in staging — Include forced disconnect and reconnect scenarios in your integration tests.
For a full production deployment guide including monitoring, see running MCP in production.
Official Sources
- MCP transports specification - official Streamable HTTP session, reconnection, resumability, and redelivery behavior.
- MCP Inspector documentation - official tool for reproducing connection and reconnection behavior outside an AI client.
Frequently Asked Questions
Does MCP define a universal reconnect operation? No. The MCP specification does not define a standard reconnect mechanism. Transport reconnection, session recovery, fresh MCP initialization, authentication refresh, and tool rediscovery are distinct concerns handled differently by each transport type and server implementation.
Why does a fresh connection work but reconnect always fails? Almost always means the server is stateful and the reconnect attempt is trying to resume a session that no longer exists — either because the server restarted, was redeployed, or the session expired.
Why do tools disappear after a successful MCP reconnect?
A transport-level reconnect does not automatically re-run MCP initialization. Confirm the client issues a full initialization sequence including tools/list after reconnecting.
What causes an MCP reconnect loop? Reconnect loops happen when the client retries aggressively without exponential backoff, the underlying problem is permanent, and nothing breaks the retry cycle.
How does a load balancer cause MCP reconnect failures? If session state is stored in-memory on individual server instances, a reconnect routed to a different instance will not find the session. Fix with sticky sessions or a shared external session store.
Will an expired OAuth token cause a reconnect failure? Yes. HTTP 401 Unauthorized. The client must obtain a fresh token before attempting to reconnect.
What does HTTP 404 mean during an MCP reconnect? It can mean the session expired, the endpoint URL changed after a deployment, or the reverse proxy routing changed. Check server logs before assuming session expiry.
How do I prevent a reverse proxy from killing idle MCP connections? Configure the proxy's idle timeout to exceed your longest expected idle period, or implement application-level keepalive pings from the client.
What is the fastest way to test MCP server reconnect behavior? Use MCP Inspector — connect, disconnect manually, reconnect, and observe whether the server handles the reconnect cleanly. If Inspector reconnects but your client does not, the problem is in your client's reconnect logic.