MCP Server Troubleshooting Guide: Fix Handshake, Session, SSE, Streamable HTTP, and Tool Discovery Errors
If your MCP server freezes during handshake, drops sessions, or hides tools from the client, it’s almost always one of the common issues. This guide covers each one with symptoms and fixes. MCP server troubleshooting works best when you stop guessing and instead follow the failure down through the transport, protocol, and application layers in order.
If you haven’t containerized your server yet, check this guide on how to deploy a remote MCP server with Docker.
Table of Contents
Understand the MCP Server Troubleshooting Workflow
Before changing config files, it helps to separate the problem into layers:
- Transport (stdio, SSE, Streamable HTTP).
- Protocol (JSON-RPC handshake and session state).
- Application (tool schemas and handlers).
Most MCP server troubleshooting sessions waste time because someone edits the tool code when the real fault is a proxy timing out a long-lived connection, or edits the nginx config when the real fault is a version mismatch in the initialization request.
The quickest way for an MCP server troubleshooting session is to reproduce the issue with the MCP Inspector, check the transport with curl, then compare container and proxy logs side by side.
This order matters; the inspector shows if it’s a client-side issue, curl confirms whether the network path works, and logs reveal what actually happened.
You can use this table as a quick reference before diving into a specific fix:
| Symptom | Likely layer | Root cause |
|---|---|---|
| Client hangs on connect, no error | Transport | Server not listening, wrong port, or stdout corrupted by logs |
| “Unsupported protocol version” | Protocol | Client and server advertise incompatible protocolVersion strings |
| “No valid session ID provided” (400) | Protocol | Session ID missing or expired between requests |
| SSE stream drops after ~60-100 seconds | Network/Proxy | Reverse proxy or CDN idle timeout killing the long-lived connection |
| Tools list is empty or missing entries | Application | Tool registration failed, or client cached a stale list |
| “Invalid params” (-32602) on tool call | Application | Tool arguments don’t match the declared input schema |
| Tool call never returns | Application/Network | No timeout handling in the handler, or upstream dependency stalled |
| 401/403 on every request | Auth | Missing or malformed Authorization/Bearer header |
Fix MCP Handshake and Initialization Errors
When a client can’t complete the first exchange, that is where MCP server troubleshooting has to start. The MCP handshake requires a three-message sequence:
- The client sends initialize.
- The server replies with its capabilities.
- And the client confirms with a notifications/initialized message before any tool calls are allowed.
1. SSH into your VPS and confirm the MCP server process is actually running:
ps aux | grep mcp
#Or
systemctl status mcp-server
2. Check the exact port it’s bound to:
ss -tlnp | grep <port>
If nothing shows, the server never started, so check its startup logs first.
3. Send a handshake request with curl:
curl -i -X POST http://localhost:3001/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}'
4. Read the response. A healthy server returns HTTP 200 with a JSON body containing serverInfo and capabilities. If the body is empty, malformed, or mixed with plain text, something is printing to stdout.
5. Search container logs for unexpected stdout output:
docker logs <container> 2>&1 | grep -v '"jsonrpc"'
Any line that isn’t valid JSON-RPC is corrupting the stdio channel.
A lot of these bugs depend on one thing: a stray print() or console.log() statement leaking into stdout. MCP servers must only send JSON-RPC messages over stdout; everything else, including logs, needs to go to stderr. Otherwise, it corrupts the channel.
6. Confirm the client sent notifications/initialized after the response, check your server log for that exact method name before it accepts any tools/call.
Resolve MCP Protocol Version Mismatches
If the server and client don’t agree on the protocol version, the handshake fails right away. This is a common MCP server troubleshooting thing to check once you’ve ruled out basic connectivity issues.
1. In the curl response from the handshake test above, check the protocolVersion field the server returned.
2. Compare it against what your client config sends, for Claude Desktop or similar, check the client’s mcpServers config or SDK version.
3. Grep your server code for the accepted version list:
grep -rn "protocolVersion" ./src
Confirm it includes the version your client is sending. Even version strings that look like dates, like 2024-11-05 vs. 2025-06-18, are treated as incompatible unless the server supports both.
4. If they don’t match, update the client SDK:
npm update @modelcontextprotocol/sdk
For Python:
pip install --upgrade mcp
Or, add the missing version string to your server’s accepted list.
5. Re-run the curl handshake test to confirm both sides now agree.
- Log both
clientInfoandprotocolVersionon every incoming initialization call. - Maintain a list of supported versions in the server code and reject anything outside it.
- Pin your SDK version so an update doesn’t silently shift the negotiated protocol.
Diagnose Broken or Expired MCP Sessions
Streamable HTTP transport is stateful, and a dropped or mismatched Mcp-Session-Id header is one of the most common causes of “No valid session ID provided” errors during MCP server troubleshooting.
1. Run the handshake and capture the session header:
curl -i -X POST http://localhost:3001/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{}}}' | grep -i mcp-session-id
2. Copy the returned Mcp-Session-Id value.
3. Send a follow-up request reusing that exact ID:
curl -i -X POST http://localhost:3001/mcp \
-H "Content-Type: application/json" \
-H "Mcp-Session-Id: <session-id-from-step-2>" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
If this returns “No valid session ID provided,” check server-side session storage for expiry settings, and increase the TTL if sessions die too fast.
4. On the client side, confirm your code persists and resends the header on every request, with retry and backoff logic, rather than blindly starting a new session on every failure.
5. Add logging on the server, so you can trace exactly where the client and server session state differ during MCP server troubleshooting.
MCP SSE Transport Failures and Proxy Buffering
Legacy SSE transport is fragile because it relies on a long-lived HTTP connection, and proxies weren’t built to keep connections open indefinitely. This is where network-layer MCP server troubleshooting becomes essential, because the server and client logs can both look perfectly healthy while a proxy silently kills the stream in between.
1. Test the SSE stream directly against the app, skipping the proxy:
curl -N -H "Accept: text/event-stream" http://localhost:3001/sse
Time how long does the connection stay open. If it drops around 60 to 100 seconds when hitting the app directly, the app is the problem. If it stays open here but drops through the proxy, the proxy is the problem.
2. Test the same request through the public domain:
curl -N -H "Accept: text/event-stream" https://your-domain.com/sse
If it fails only externally, edit your nginx config:
location /sse {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
Test the config and reload:
sudo nginx -t && sudo systemctl reload nginx
3. Re-run the external curl test to confirm the stream now stays open past the previous cutoff. Common culprits behind SSE timeouts include nginx’s 60-second proxy_read_timeout, AWS ALB’s 60-second idle timeout, and Cloudflare’s fixed 100-second SSE limit on the free tier.
4. If you’re on Cloudflare’s free tier, upgrade or switch to Streamable HTTP instead. Also, make sure to disable proxy_buffering; buffered proxies queue up SSE events instead of streaming them right away, which looks just like a hung connection when you’re on the MCP server troubleshooting.
These same buffering and timeout principles carry over directly if your MCP server runs behind a tunnel-based proxy setup. The Cloudflare Tunnel troubleshooting guide covers that scenario in more depth and is worth reviewing if that’s your setup.
Fix MCP Streamable HTTP Transport Mismatches
Streamable HTTP combines everything into a single POST endpoint, which can respond with a plain JSON body or upgrade to an SSE stream. It’s the transport most new MCP servers should use by default.
1. Check which route your server actually mounts:
grep -rn "app.post\|app.get" ./src | grep -i mcp
Make sure your client config points to the right path. If it’s still pointed at an SSE-only path like /sse, but the server only mounts Streamable HTTP at /mcp, you’ll get a 404.
2. Test directly:
curl -i -X POST http://localhost:3001/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
If you get a 404, update the client’s endpoint URL to match the server’s actual mount path, then restart the client and reconnect.
If you get a 200 but no stream when you expect one, check that the Accept header includes both application/json and text/event-stream. Leaving either one out can cause the server to pick the wrong response mode.
| Transport | Endpoint pattern | Best for |
|---|---|---|
| stdio | N/A (spawned process) | Local desktop clients |
| SSE (legacy) | GET /sse + POST /messages | Older clients, being phased out |
| Streamable HTTP | Single POST /mcp | Remote servers, firewall-friendly |
Troubleshoot MCP Tool Discovery Errors
If the client connects successfully but the tools panel is empty, MCP server troubleshooting shifts from the transport layer to the application layer.
1. Open the MCP Inspector and connect it to your server:
npx @modelcontextprotocol/inspector http://localhost:3001/mcp
2. In the Inspector UI, click “List Tools” and compare the results to what you expect.
3. If tools are missing, check server startup logs for exceptions:
docker logs <container> --tail 100 | grep -i error
4. If a tool throws an error during registration, it fails silently. To catch this, wrap each tool registration in a try/catch and log the failure:
try:
register_tool(my_tool)
except Exception as e:
print(f"[ERROR] Failed to register tool: {e}", file=sys.stderr)
5. Restart both client and server to clear any cached tool list. Kill the client process and run:
docker restart <container>
Then, reconnect.
6. Re-run tools/list in the Inspector to confirm the full set now shows up. This one step fixes most MCP server troubleshooting cases of stale or partial tool lists.
MCP Schema Validation Errors
A tool call failing with -32602 Invalid params means the client sent arguments that don’t match the tool’s JSON schema. Instead of guessing what’s wrong, check the error’s params field against your schema definition directly, which usually points you to the fix.
1. Reproduce the failing call directly with curl using the exact arguments the client sent:
curl -X POST http://localhost:3001/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"my_tool","arguments":{"query":"test"}}}'
Read the -32602 Invalid params error body, it should indicate which field failed.
2. Pull your tool’s declared schema: check the inputSchema object in your tools/list response or source code.
Check whether the argument types and required fields match what the client actually sent.
3. Fix the schema, loosen a type that’s too strict, or fix the client call, send the right shape, then re-run the same curl command to confirm you get a clean 200 response.
Resolving MCP Request Timeout Failures
MCP does not define a standard timeout at the protocol level, so each client and host enforces its own limit, making timeout-related MCP server troubleshooting inconsistent across tools.
1. Add timing instrumentation to the suspected slow tool handler:
import time, sys
start = time.time()
result = run_tool_logic(args)
duration = time.time() - start
if duration > 10:
print(f"[WARNING] tool call took {duration:.1f}s", file=sys.stderr)
2. Call the tool and watch stderr for the warning:
docker logs -f <container>
If the tool itself is slow, profile the upstream dependency it calls (database, external API) rather than the MCP layer.
If the tool returns quickly but the client still times out, check the client’s own configured timeout value and increase it in its config file.
If using Streamable HTTP behind a proxy, also raise proxy_read_timeout in nginx, as shown in the SSE section, since a proxy timeout can masquerade as a tool timeout during MCP server troubleshooting.
MCP Authentication Failures | Diagnose 401 and 403 Responses
A server that returns 401 or 403 on every request usually has a missing, expired, or malformed Authorization header rather than a broken tool or transport.
1. Test with curl and a header first, bypassing the proxy:
curl -i -X POST http://localhost:3001/mcp \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
2. If this succeeds directly on the app but fails through the public domain, run the same test on the proxied URL:
curl -i -X POST https://your-domain.com/mcp -H "Authorization: Bearer YOUR_TOKEN" ...
3. If it only fails externally, the proxy is likely stripping the header on its way through. This is a common cause of confusing MCP server troubleshooting reports, where the server logs never even show the request.
Fix it by adding the header in nginx:
location /mcp {
proxy_pass http://localhost:3001;
proxy_set_header Authorization $http_authorization;
proxy_pass_request_headers on;
}
Reload nginx:
sudo nginx -t && sudo systemctl reload nginx
4. Re-run the external curl test to confirm the header now reaches the app: check your app logs to see the Authorization value logged on the incoming request.
A Complete Debugging Workflow for MCP
Three tools solve nearly every issue covered in this MCP troubleshooting guide, including the MCP Inspector, curl, and reading container/proxy logs side by side.
- MCP Inspector: shows the handshake and capability negotiation, and lets you test individual tool calls with sample inputs.
- curl: confirms the server is reachable and returns the right status codes, even before opening a client.
- Container logs: show stdout corruption, crashes, and startup errors that never reach the client.
- Proxy logs (nginx, Cloudflare, ALB): show idle timeouts, buffering issues, and stripped headers that can look like application bugs.
1. Launch the Inspector on the fixed server:
npx @modelcontextprotocol/inspector http://your-domain.com/mcp
Use a local server command instead of a URL for stdio servers.
2. Walk through the full flow in order, connect, list tools, and call one tool, confirming each step succeeds in the Inspector UI.
3. Tail both application and proxy logs side by side in separate terminals:
docker logs -f <container>
sudo tail -f /var/log/nginx/error.log
4. Run the original failing action from your real client one more time and watch both log streams simultaneously to confirm no errors reappear.
Running MCP Servers on a Dedicated Host
Desktop environments introduce variables that make MCP server troubleshooting harder than it needs to be, inconsistent PATH values, sleeping laptops killing long connections, and local firewalls blocking ports. Running your MCP server on a stable Linux VPS removes the desktop as a variable, so any remaining failure is isolated to the server, proxy, or client configuration itself.
A dedicated VPS also gives you full control over nginx or Caddy configuration, systemd logs, and firewall rules, all of which show up repeatedly in the steps above.
Conclusion
Most MCP server failures fall into a small number of repeatable patterns:
- A broken handshake.
- An expired session.
- A proxy that can’t handle long-lived connections.
- A schema mismatch on a tool call.
If you work through the layers in order in this MCP troubleshooting guide, including transport, protocol, and application, you can turn a confusing failure into a simple diagnosis.
We hope you enjoy this guide. For the official transport specification, you can check the MCP Streamable HTTP transport documentation.
FAQs
What is the fastest way to start MCP server troubleshooting?
Connect with the MCP Inspector first. It shows the handshake and tool list in one place before you use curl or logs.
Why does my MCP server disconnect after about a minute?
A reverse proxy or CDN is likely closing the idle connection. Check nginx proxy_read_timeout or your CDN’s SSE limits.
How do I debug a 401 error on every MCP request?
Test with curl and an Authorization header first. If curl works but your proxy fails, the proxy is stripping the header.