How to Fix an MCP Server Connection Closed Error
Fix MCP “connection closed” errors by identifying the transport, checking startup and stdout, validating negotiation, and isolating HTTP or SSE failures.
An MCP Connection closed error does not have one universal cause. The fix depends on whether the client uses local stdio, Streamable HTTP, or SSE, and whether the process closes during startup, protocol negotiation, authentication, or an established session.
Start by recording the exact error, client and version, server command or URL, transport, and failure stage. Then follow the matching branch below.
1. Identify the transport and failure stage
Write down:
- The complete error text, including nested errors.
- The host and version, such as Claude Desktop, Claude Code, Cursor, or another MCP client.
- The server command and arguments, or the remote endpoint.
- The transport: local
stdio, Streamable HTTP, or SSE. - Whether it fails immediately, during initialization, after a tool call, or after being idle.
| What you observe | Most useful first check |
|---|---|
| Connection closes immediately after launch | Run the exact command in a terminal; inspect exit code, stderr, executable path, environment variables, and working directory. |
| Server starts but initialization fails | Check protocol negotiation, supported versions, and malformed JSON-RPC output. |
| HTTP status or authentication error | Check the URL, credentials, headers, proxy, TLS, and server logs. |
| SSE stream disconnects | Inspect keepalive behavior, proxy idle timeouts, and reconnect handling. |
| Works in Inspector but not in a host | Compare launch environment, PATH, working directory, and configuration exactly. |
2. Fix a local stdio server
With stdio, the host reads stdout as the JSON-RPC protocol stream. Any banner, debug line, progress message, or stack trace written to stdout can make the stream invalid and cause the client to close the connection.
Keep stdout protocol-only
Send diagnostics to stderr:
// server.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
const server = new Server(
{ name: "example", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
console.error("MCP server starting"); // Correct: stderr
// console.log("MCP server starting"); // Wrong: corrupts stdio JSON-RPC
// Register handlers, then connect using the stdio transport.
For Python, use print(..., file=sys.stderr) for logs:
import sys
print("MCP server starting", file=sys.stderr)
Run the exact configured command
# Replace these values with the command from your host configuration
/path/to/node /absolute/path/server.js
printf 'exit=%s\n' "$?"
If the process exits, fix that underlying error first. Common causes include a missing module, an invalid argument, an unavailable environment variable, a permissions error, or a runtime version mismatch.
Use absolute paths and explicit environment values
Desktop hosts often start processes with a smaller PATH and a different working directory than your interactive shell. Use an absolute executable and script path. Confirm required variables are available to the host, not only in your shell profile.
which node
node --version
pwd
printenv | sort
Compare this output with the host’s MCP logs or launch configuration. If a relative path works in a terminal but not in the host, convert it to an absolute path and set the required working directory explicitly.
Check permissions and runtime versions
- Make sure the configured executable is runnable.
- Use the same Node.js, Python, or other runtime version in the host and terminal.
- Confirm dependencies are installed in the environment the host actually uses.
- Do not rely on shell startup files to initialize PATH or secrets unless the host documents that behavior.
3. Validate protocol negotiation
After the process starts, the client and server negotiate protocol capabilities and versions. A negotiation failure can look like a closed connection when the server exits during the probe or when a pinned version is not offered.
- Read the complete initialization error rather than only the final “closed” line.
- Check the server’s advertised protocol versions and the client’s requested version.
- Remove an unnecessary pinned version and allow automatic negotiation when the SDK supports it.
- If a deployment must use an older supported version, configure that version on both sides.
- If a custom transport fails before initialization, reproduce with the SDK’s base stdio transport.
These options are SDK-specific. Apply the setting documented for your client and SDK version; do not copy a TypeScript option into an unrelated implementation.
4. Diagnose Streamable HTTP and SSE
Remote connections have additional failure branches. Capture the HTTP status, response headers, body, and server logs before changing protocol settings.
Test the endpoint directly
curl -i --connect-timeout 10 --max-time 60 \
-H 'Accept: text/event-stream, application/json' \
-H 'Authorization: Bearer YOUR_TOKEN' \
https://example.com/mcp
Interpret the result:
- 401 or 403: credentials, scopes, or authorization headers are wrong or missing.
- 404: the endpoint path or deployment route is incorrect.
- 400: required headers or protocol negotiation fields are malformed.
- 5xx: inspect server and reverse-proxy logs; this is a deployment failure until proven otherwise.
- TLS or DNS error: fix certificates, DNS, firewall, or proxy configuration.
- Connection drops after idle time: inspect keepalives and proxy idle limits.
Keep SSE connections alive
The TypeScript SDK troubleshooting guide describes SSE keepalive comments every 15 seconds by default and exposes a keepAliveMs setting. Confirm the value for your installed SDK and ensure your proxy allows the stream to remain open. Other clients and servers may use different defaults.
Configure keepalive below the shortest idle timeout in your load balancer, reverse proxy, firewall, and hosting platform. Also implement reconnect handling where the client supports it.
Separate a network close from a protocol mismatch
An HTTP drop, proxy-generated 5xx response, or socket reset is evidence for connectivity or deployment diagnosis. It is not, by itself, evidence of a protocol-version mismatch.
A Claude Code issue opened on August 10, 2026 reports a clean HTTP close after 420 seconds followed by reconnection in that environment. Treat that as one client report, not a universal MCP timeout.
5. Reproduce with MCP Inspector
MCP Inspector is a diagnostic client for testing an MCP server. Run the server there, then compare its behavior with the failing host.
- Launch Inspector using the same server command or endpoint.
- Use the same environment variables, credentials, working directory, and runtime.
- Record whether initialization completes and whether tools remain available.
- Compare Inspector logs with the host’s logs line by line.
Inspector success does not prove the host configuration is correct. A different PATH, executable lookup, current directory, or secret source can explain the difference.
6. A repeatable diagnostic checklist
- Copy the exact error and timestamp.
- Identify stdio, Streamable HTTP, or SSE.
- Determine whether the close occurs at launch, initialization, during a request, or after idle time.
- Run the local command directly and check the exit code.
- Move every human-readable stdio log to stderr.
- Replace relative paths and implicit environment assumptions with explicit values.
- Check protocol versions only after process and network health are confirmed.
- For HTTP, capture status, headers, body, TLS, proxy, and authentication evidence.
- Reproduce in MCP Inspector with the same launch environment.
- Retest one change at a time and keep the successful configuration.
7. Common errors, causes, and fixes
| Error or symptom | Likely cause | Fix |
|---|---|---|
| “Connection closed” immediately after launch | Process crashed or exited | Run the exact command manually; fix the first startup error. |
| Unexpected token or invalid JSON | Logs or a banner on stdout | Send logs to stderr and keep stdout JSON-RPC only. |
| Works in terminal, fails in host | Different PATH, CWD, runtime, or environment | Use absolute paths and compare host and terminal environments. |
| Initialization or version negotiation failure | Unsupported or pinned protocol version | Use compatible versions or automatic negotiation as documented by the SDK. |
| 401/403 from remote endpoint | Missing, expired, or insufficient credentials | Verify authorization headers, token scope, and server authentication logs. |
| SSE “terminated” or socket reset | Proxy, firewall, TLS, or idle timeout | Inspect network logs, enable keepalive, and configure reconnects. |
| 5xx during initialization | Server or reverse-proxy failure | Check deployment logs and reproduce with curl and Inspector. |
8. Reliability and performance notes
- Keep startup deterministic: validate configuration, then start the protocol transport without printing to stdout.
- Prefer absolute paths and pinned runtime dependencies in desktop configurations.
- Use structured stderr logs containing timestamps, request IDs, and failure stages.
- For remote streams, set keepalive below infrastructure idle limits and make reconnect behavior explicit.
- Do not increase timeouts to hide a process crash, malformed JSON, authentication failure, or protocol mismatch.
- When diagnosing latency, separate process startup, initialization, network negotiation, tool execution, and server-side work.
9. Or skip the browser setup
If the MCP server is only needed to capture website screenshots, ScreenshotNeo provides a hosted screenshot API and an MCP server. One GET request returns a PNG, JPEG, WebP, or PDF, so there is no browser process or local stdio server to keep alive.
See the ScreenshotNeo API documentation for all options. A minimal request is:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and each response reports the result through X-Page-Verdict and X-Billed headers. Its MCP server includes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.
The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account.
10. FAQ
Does “connection closed” always mean the server crashed?
No. A client can report the same final message for a crashed stdio process, malformed protocol output, failed negotiation, authentication rejection, proxy failure, or an intentionally closed idle stream.
Why does adding a print statement break stdio?
Because stdout is the JSON-RPC channel. Human-readable text makes the next message invalid JSON. Write diagnostics to stderr.
Should I pin an MCP protocol version?
Only when your client and server require a known compatible version. Otherwise use the SDK’s supported automatic negotiation behavior.
Is a 420-second timeout part of MCP?
No universal timeout is established by the cited sources. The 420-second value comes from one Claude Code issue report and should be treated as an environment-specific observation.
What evidence should I include when reporting the bug?
Include the exact error, transport, client and server versions, launch command or endpoint, timestamps, exit code, stderr, HTTP status and headers, and whether MCP Inspector reproduces it.


