The 200 that lies
If you point a standard uptime monitor at an MCP server's HTTP endpoint, there's a good chance it'll report green even while the server is functionally broken. That's not a bug in the monitor — it's a mismatch between what HTTP status codes mean and what MCP actually needs to guarantee.
MCP runs on JSON-RPC 2.0, typically over a streamable HTTP transport (or SSE, or stdio for local servers). The HTTP layer and the protocol layer are separate concerns, and a 200 only tells you about the first one.
Two layers, two very different failure modes
The transport layer answers: did the server accept the connection and respond at all? This is what a 200 status code confirms. It says nothing about whether the response body is a valid MCP message, whether the requested method succeeded, or whether the server's tool contract matches what the client expects.
The protocol layer answers: did the actual MCP exchange succeed? This is encoded inside the JSON-RPC response body, not the HTTP status. A perfectly healthy-looking 200 response can carry:
{
"jsonrpc": "2.0",
"id": 7,
"error": {
"code": -32602,
"message": "Invalid params: missing required field 'query'"
}
}
That's an HTTP success wrapping a protocol failure. A monitor that only checks the status code will mark this server healthy indefinitely.
Where this actually bites teams
A few real patterns show up repeatedly:
- Initialize succeeds, tools/list doesn't. The connection and handshake work fine, but the follow-up call to list tools times out or errors, meaning no client can actually discover what the server does.
- Capability mismatch. The server reports a protocol version or capability set in
initializethat its actual tool implementations don't support, so calls fail downstream even though the handshake itself returned 200. - Silent tool failures. A
tools/callrequest returns 200 with a JSON-RPC result, but the result'sisErrorflag is set, or the content doesn't match the tool's declared output schema. Nothing at the transport layer flags this. - Schema drift with a healthy transport. The server is entirely reachable and responsive; it just quietly changed a parameter type or removed a tool between deploys. Every health check built around status codes stays green through the entire incident.
What an actual MCP health check needs to verify
A meaningful check has to parse and validate the message, not just the response code:
- Complete the handshake. Send
initialize, confirm a valid capabilities response, sendnotifications/initialized. - Call
tools/list. Confirm it returns without a JSON-RPC error and that the shape matches expectations (an array of tool definitions with names, descriptions, and input schemas). - Validate the JSON-RPC envelope. Check for
jsonrpc: "2.0", a matchingid, and the absence of an unexpectederrorobject. - Compare against a baseline. Even a fully successful response can represent a regression if the tool schemas inside it changed since the last check.
- Measure protocol-level latency, not just connection time — the gap between sending
initializeand receiving a complete, valid response.
Building this yourself vs. not
You can build this with a scheduled script: open a connection, run the handshake, call tools/list, diff the result, and alert on either a protocol error or an unexpected schema change. It's a reasonable weekend project for a single server.
Where it gets tedious is doing this consistently across every server you run, keeping historical baselines so you can tell what changed and when, and having somewhere other than a private script's logs to see current status. That's the specific gap MCP Vitals targets — it runs the real handshake and tools/list call against your servers on a schedule, tracks schema changes over time, and turns the result into a status badge and alerts instead of a green checkmark that only means "something answered."
The takeaway
A 200 tells you the door opened. It doesn't tell you whether the room behind it is the one your clients expect. For MCP servers specifically — where the entire value is a structured tool contract — checking the status code and calling it monitoring is close to not monitoring at all. Check the handshake. Check tools/list. Check that the shape of things hasn't quietly changed.