MCP Server Health Check: What a 200 Proves, and What to Call Instead
A /health 200 says the process is up, not that MCP works. The one request that proves an MCP server is healthy on 2025-11-25 and on 2026-07-28, and how to put it in your uptime monitor.
A health check should fail when the thing your users depend on fails. For an MCP server that thing is the protocol: a client POSTs JSON-RPC and expects a JSON-RPC answer. A /health route returning 200 proves your process is up and your web framework is routing. It does not prove the MCP endpoint answers, and it can stay green while every client fails.
Our own server shows the gap. A plain GET https://mcp-hunter.com/mcp returns 200 with an HTML page about the server, because that URL is also a web page for people. The same URL asked for an event stream returns 405. Neither of those says whether MCP works. Only a JSON-RPC request does.
TL;DR:
- Check with one MCP request, not a status route. It is the only probe that fails when the protocol fails.
- On 2025-11-25 servers, send
initialize. It needs no session, and a healthy answer containsprotocolVersion[1]. - On 2026-07-28 servers, send
server/discover. Every server on that revision must implement it, andpingno longer exists there [3][4]. - A
401with aWWW-Authenticateheader is a healthy guarded server, not an outage [5]. So is a405on GET [1].
Why ping is not the answer any more
The 2025-11-25 spec has a ping request, and on paper it is the health check: the receiver "MUST respond promptly with an empty response" [2].
{"jsonrpc":"2.0","id":"123","method":"ping"}
Two things make it a poor monitor probe. On 2025-11-25 it is meant for an open connection, and a server that issues sessions should refuse a request that arrives without one [1], so a bare ping from a monitor can fail on a perfectly healthy server. And the 2026-07-28 revision removed ping altogether [3]. A probe built on it breaks the day your server upgrades.
Our server answered a bare ping with {"jsonrpc":"2.0","id":3,"result":{}} on 9 October 2026, because it does not require a session. Yours may not, which is the problem with a probe that depends on it.
The check, by protocol revision
Almost every server today is on the older revision. Of 694 registry servers that completed a handshake in our census on 20 August 2026, 382 negotiated 2025-11-25 and 3 negotiated 2026-07-28. If you do not know which yours speaks, the curl guide shows how to tell.
2025-11-25 and older: initialize
curl -sS --max-time 10 https://your-server.example.com/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"health-check","version":"1.0.0"}}}'
Healthy means a result that contains protocolVersion. Ours, shortened:
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"tools":{"listChanged":false}},"serverInfo":{"name":"MCP Hunter","version":"1.0.0"}}}
initialize is the one request every handshake-era server must accept without a session, which is why it beats ping as a probe. The cost: if your server issues sessions, each check opens one. The spec says a client that no longer needs a session should end it with an HTTP DELETE carrying the Mcp-Session-Id header [1]. At one check a minute that is 1,440 sessions a day, so either send the DELETE or make sure idle sessions expire.
2026-07-28: server/discover
curl -sS --max-time 10 https://your-server.example.com/mcp \
-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/clientCapabilities":{},"io.modelcontextprotocol/clientInfo":{"name":"health-check","version":"1.0.0"}}}}'
Healthy means a result that contains supportedVersions. There is no session on this revision [3], so the check leaves nothing behind.
When the check should go further
Both requests prove the server answers MCP. Neither proves it has tools. If a broken deploy can bring the server up with an empty tool list (a missing environment variable that disables a plugin, say), add tools/list and alert when the count drops. The tools/list reference shows the request and the fields to count.
Answers that look broken and are healthy
| You got | What it means |
|---|---|
401 with a WWW-Authenticate header |
The server is up and guarding itself. The spec has a guarded server put the address of its authorization metadata in that header when it returns 401 [5] |
405 on a GET |
Normal. A 2025-11-25 server may refuse GET [1], and 2026-07-28 removed the GET stream [3]. Health checks use POST |
200 with Content-Type: text/event-stream |
A normal answer sent as a stream, which a server may always do [1]. The JSON is on the data: lines |
-32601 on server/discover |
The server is on the handshake revisions. Use initialize instead |
The 401 matters most, because it is common. In the same census, 263 of 1,100 registry addresses (23.9%) answered by asking for credentials. A monitor that treats 401 as down would page you for a quarter of all working servers. If yours is guarded, either give the monitor a token or accept a 401 that carries the WWW-Authenticate header as up, knowing it proves the auth layer answered and not what sits behind it.
And the failures that are real: a timeout, a refused connection, a DNS error, a 5xx, an HTML page in answer to a POST, or a JSON-RPC error on initialize. In the census, 139 of 1,100 addresses (12.6%) did not answer as an MCP server at all.
Putting it on a schedule
Any monitor that can send a POST with a JSON body and custom headers, and match a word in the response, can run these checks: set the method to POST, paste the body and the two headers from above, and alert unless the response contains protocolVersion (or supportedVersions). Uptime Kuma's HTTP keyword monitor does this, and so do most hosted uptime services.
For a script your scheduler or CI can run, this exits non-zero when the server is unhealthy:
#!/usr/bin/env bash
URL=${1:?usage: mcp-health.sh <endpoint-url>}
BODY='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"health-check","version":"1.0.0"}}}'
curl -sS --max-time 10 "$URL" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d "$BODY" | grep -q '"protocolVersion"'
We do not monitor anyone's server, so the schedule is yours to run. What we offer is the one-off version.
A one-off check from outside your network
A check from your own machine can pass while strangers fail, because your machine reaches what the internet does not: localhost, your VPN, a host behind your firewall. Our MCP connection test runs the same probe from our network: server/discover first, then the handshake if the server needs it, then tools/list. It reports which revision answered and which tools came back, or which of the failures above it hit, and saves the result at its own dated URL you can send to someone.