MCP server won't connect? Work through the error you get
Our probe connects to every endpoint we monitor the way a new client would: no account, no token, at most 8 requests per run. When a connection fails, the report names the step and a reason. Below are those reasons in the order a connection meets them, how often each showed up, and a command to see the same thing on your own server.
Where 2,980 endpoints stopped
We took the latest scheduled run of each of the 2,980 endpoints our probe checked between 2026-10-01 13:00Z and 2026-10-08 13:00Z, and asked where each connection stopped first.
| Where it stopped | Reason on the report | Endpoints |
|---|---|---|
| Nowhere: handshake done, tools listed | none | 1,802 |
| Handshake: a key was required | credential_required |
965 |
Handshake: initialize got a non-200 status |
handshake_initialize_http_error |
115 |
| Handshake: redirect or 5xx on the first request | handshake_discover_http_error |
61 |
| Handshake: closing notification refused | handshake_ack_http_error |
6 |
| Handshake: anything else | two reasons, one endpoint each | 2 |
| Tool list: a key was required | credential_required |
8 |
| Tool list: anything else | five reasons | 6 |
| The first exchange never finished | four reasons, below | 15 |
Of the 1,178 endpoints that stopped somewhere, 1,149 stopped at the handshake, and 965 of those did so on purpose. When the handshake fails, protocol_revision and usually tools_list fail with it. Fix the handshake first.
Send what our probe sends
Our probe first sends the current handshake, server/discover. If the answer is a 4xx, or a 2xx that is not a discovery result, and it carries none of the three rejection codes the current revision defines, the probe falls back to the older initialize. You can send both:
URL=https://your-server.example/mcp
INIT='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl-check","version":"0.1.0"}}}'
curl -sS -i "$URL" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: server/discover' \
-d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl-check","version":"0.1.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}'
curl -sS -i "$URL" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d "$INIT"
A working server on the older revision answers the second command with HTTP 200 and a JSON-RPC result holding a protocolVersion string, as plain JSON or in a data: line of an event stream. Neither command sends credentials or follows redirects, the same as our probe. If your firewall treats unknown clients differently, add -A 'MCPCheckup-Probe/1.0 (+https://mcpcheckup.com/probe)', the user agent our probe sends.
A key was required
Your server answered 401 with a WWW-Authenticate header naming an authentication scheme. We never send credentials, so the handshake and tool list are Not checked, not Failed, and the report says this looks like deliberate access control. If that was your intent, there is nothing to fix. What we can still observe about these endpoints has its own post. To see your challenge:
curl -sS -o /dev/null -D - "$URL" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d "$INIT" | grep -i -E '^(HTTP|www-authenticate)'
Of the 115 endpoints whose initialize got a non-200 status, 65 got a 401 without a WWW-Authenticate challenge we could read. Without one, a 401 is a refusal with no instructions, and the handshake is recorded as Failed. If your server needs a key, send the challenge, for example WWW-Authenticate: Bearer resource_metadata="https://your-server.example/.well-known/oauth-protected-resource". How we tell a refusal from a failure goes through the cases.
initialize got a non-200 status
The report reads: "Our server/discover request got neither a discovery result we could use nor a protocol error defined by the current MCP specification, so we tried the older initialize handshake. The server answered initialize with HTTP 404 instead of 200."
| Status | Endpoints | What it usually means |
|---|---|---|
| 401 | 65 | a refusal without a challenge |
| 404 | 28 | nothing answers a POST at this address |
| 403 | 6 | a firewall or access rule said no |
| 202 or 201 | 4 | accepted, but no answer in the body |
| 410 | 3 | the address is gone |
| 405 | 2 | the address does not take POST |
| 402 or 451 | 4 | your server's own policy answer |
| 500 | 2 | the server failed on initialize |
| 400 | 1 | the request was rejected as malformed |
For 404, 405 and 410, the listed address is not where your MCP server listens: often the site root when the server answers on /mcp, or the other way round. Run the initialize command against the exact address you listed; a page loading in a browser proves nothing. For 403, add our user agent: if that alone turns a 200 into a 403, a bot rule is in the way. A 202 or 201 with no JSON-RPC answer often means a server built for the older HTTP+SSE transport, which sends the answer on a separate event stream. Our probe expects it in the response to the POST.
The first request got a redirect or a 5xx
The report reads: "We sent a server/discover request. The server answered HTTP 502. The probe version that ran this check did not treat that status as a discovery result or as a reason to try the older initialize handshake, so the handshake stopped there."
Of these 61 endpoints, 30 got a 502, 16 a redirect (7 a 307, 6 a 308, 3 a 301), 11 a status from 523 to 530, and 4 a 500 or 503.
Redirects. Our probe never sends a POST again to a new address, and neither does curl without -L. To see where you are sent:
curl -sS -o /dev/null -w '%{http_code} %{redirect_url}\n' "$URL" \
-H 'Content-Type: application/json' -d "$INIT"
Common causes are a missing or extra trailing slash, or http:// against https://. List the final address, or serve MCP where your listing points.
502, 500, 503. A gateway in front of your server got no answer from it. Check whether the process behind it was running.
523 to 530. A CDN returns these non-standard statuses when it cannot reach your origin or cannot complete a TLS handshake with it. Our own tls_certificate check is Not checked on every run, because validating a certificate chain needs a different executor than our fetch-based probe, so look yourself:
curl -v -o /dev/null "$URL" 2>&1 | grep -E '^\*'
The lines starting with * show the TLS handshake, the certificate's subject and expiry, and whether verification succeeded. Behind a CDN, run it against the origin's own address too.
The closing notification was refused
The report reads: "The server accepted our initialize request (older handshake), but answered the notifications/initialized message that completes the handshake with HTTP 400 instead of a 2xx." Six endpoints: three with 400, two with 404, one with 500.
If initialize returned an Mcp-Session-Id header, our probe sends it back here. A 400 or 404 often means the session was not found, for example when several instances share no session store. The usual answer to a notification is 202:
curl -sS -i "$URL" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'Mcp-Session-Id: SESSION_ID_FROM_INITIALIZE' \
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
The first exchange never finished
Fifteen endpoints: eleven ran out of time, one host name did not resolve, one server asked us to come back later, and for two our own DNS resolver did not answer, which needs nothing from you.
Time. A run has 10 seconds in total. This comes and goes: across all runs by our current probe version, 25 endpoints ran out of time before the first exchange finished at least once, against 11 on their latest run. Common causes are cold starts and a slow first byte. Measure after the server has been idle, not right after a deploy:
curl -sS -o /dev/null -w 'tls %{time_appconnect}s, first byte %{time_starttransfer}s, total %{time_total}s\n' "$URL" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d "$INIT"
DNS. dig +short your-server.example A and the same with AAAA should return addresses.
Come back later. The report reads: "The server told us to come back later (HTTP 429, or 503 with Retry-After), so we stopped this round before finishing the checks." The Retry-After values we saw ran from 5 seconds to 43,169 seconds, about 12 hours. If you limit by client, one run is at most 8 requests.
Before you check again
The box on our homepage runs the same checks on any address you paste. The result is not signed, and a host gets at most one on-demand check every 10 minutes, so make the change, wait, then check. What that check runs has its own post, the methodology page explains the four states, and the probe page says what our probe sends.
How we counted
- Population: every endpoint with at least one scheduled run (homepage checks excluded) that started between 2026-10-01T13:00Z and 2026-10-08T13:00Z, 2,980 in all, each counted once by its latest such run. Counted at 2026-10-08T13:17Z.
- All 2,980 latest runs came from the probe version live since 2026-10-06T21:10Z. The week had 36,702 scheduled runs and four version changes; three handshake reasons appear only before 2026-10-03, when the probe stopped where it now tries
initialize. - "Where it stopped" reads reachability, the handshake and the tool list, in that order. The first that is Failed or Not checked decides the line.
- Status codes are those recorded with each reason. The "at least once" count and the
Retry-Afterrange cover all scheduled runs by the current probe version, up to 2026-10-08T13:00Z. - Limits: one run is one moment, and an intermittent problem shows on some runs only. We see what our network sees, and nothing behind a 401.