Menu

MCP server won't connect? Work through the error you get

2026-10-12MCP Checkup

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-After range 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.