/ guides / monitor-mcp-server
How to monitor an MCP server
Check a remote MCP server with an HTTP POST that sends a real JSON-RPC request, and a local stdio server with a heartbeat behind a scheduled smoke test.
Updated
To monitor an MCP server, send it a real protocol request on a schedule and check the answer, not just the status code. For a remote server that means an HTTP POST with a JSON-RPC body, an expected status of 200 and a keyword only a working reply contains. For a local stdio server it means a scheduled smoke test that pings a heartbeat. Fettle is uptime monitoring for developers that can do both with ordinary checks: an HTTP monitor with a custom method, headers, body and keyword, and a heartbeat for anything with no URL.
Fettle has no MCP-specific check type. Everything below uses the same HTTP check you would point at a REST endpoint, so it works the same for an MCP server written in any language. If you are new to the basics, what uptime monitoring is covers them; the request-and-keyword approach is the same one in the API health check guide.
Why isn't a GET health check enough?
Because an MCP server can answer every HTTP request and still be broken. The protocol is
JSON-RPC carried in POST requests, and a protocol error is usually an HTTP 200 with an error
object in the body. A GET to /mcp tells you the reverse proxy is up, not that the server can
complete a handshake.
A server that does not know a method answers with a JSON-RPC error such as
"error":{"code":-32601,…}, and many servers send that error with status 200. A monitor that only
expects 200 would call that a success. Adding a keyword that appears only in a successful result
closes the gap.
Which request should the monitor send?
It depends on which protocol revision the server speaks. Servers on the 2025 revisions answer an
initialize request, and their reply contains "protocolVersion". Servers built for the
2026-07-28 revision
drop the handshake and must answer server/discover, whose reply contains "supportedVersions".
Not sure which one yours speaks? Send each request once with curl and keep the one that answers.
For a 2025-era server (initialize, as defined in the
2025-11-25 lifecycle),
create an HTTP monitor with these fields. This is the body for POST /v1/monitors; the dashboard
form has the same method, header, body and keyword fields.
{
"name": "MCP server",
"type": "HTTP",
"target": "https://mcp.example.com/mcp",
"interval_seconds": 300,
"timeout_seconds": 10,
"regions": ["eu-central"],
"config": {
"method": "POST",
"request_headers": {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream"
},
"request_body": "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2025-11-25\",\"capabilities\":{},\"clientInfo\":{\"name\":\"uptime-check\",\"version\":\"1.0\"}}}",
"expected_status_codes": [200],
"keyword": "\"protocolVersion\""
}
}
If the server does not support the version you send, the spec has it answer with one it does
support, so the reply still contains "protocolVersion" and the check stays green.
For a 2026-era server, the
Streamable HTTP transport
requires two extra headers that must match the body: MCP-Protocol-Version and Mcp-Method. A
missing or mismatched header is a 400, which fails the check, so set them exactly:
{
"method": "POST",
"request_headers": {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
"MCP-Protocol-Version": "2026-07-28",
"Mcp-Method": "server/discover"
},
"request_body": "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"server/discover\",\"params\":{\"_meta\":{\"io.modelcontextprotocol/protocolVersion\":\"2026-07-28\",\"io.modelcontextprotocol/clientInfo\":{\"name\":\"uptime-check\",\"version\":\"1.0\"},\"io.modelcontextprotocol/clientCapabilities\":{}}}}",
"expected_status_codes": [200],
"keyword": "\"supportedVersions\""
}
A server that supports both revisions answers either request.
A few details of how Fettle runs this request matter here. The keyword match is case-sensitive
and plain substring, so the quotes around protocolVersion keep it from matching a word in an
error message. Fettle does not send an Accept header unless you set one, and the spec requires
both content types in it. The status code is checked before the keyword, so a 401 or 400 fails
with the status code in the error, not as a missing keyword. On Free the check runs
every 3 minutes from 1 region; paid plans add shorter intervals and more regions (pricing).
What if the server answers with a stream?
Then the check works only if the server ends the stream after its reply. A Streamable HTTP server
may answer a POST with text/event-stream instead of plain JSON. The keyword still matches inside
the data: line, but Fettle reads the body until it ends, up to 1 MiB, so an open stream runs
into the timeout.
Both spec revisions say the server should close the stream once the response is sent, and most SDKs do. You can see what yours does with curl, which prints the stream as it arrives:
curl -N https://mcp.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":"uptime-check","version":"1.0"}}}'
If curl returns right after the data: line, the monitor will work. If it hangs, you have
three options, best first: configure the server to answer single requests with
application/json if your SDK offers a JSON-response mode, make it end the stream after the
response, or drop the keyword. Without a keyword Fettle decides on the status code alone and stops
caring about the body, which also means a 200 carrying a JSON-RPC error counts as up.
The 2025-11-25 transport
also lets a server close the connection early and have the client reconnect for the result. A
single-request check cannot follow that; it sees no protocolVersion and reports the keyword
missing. A server built that way needs the stdio-style smoke test below instead.
Can the monitor call tools/list?
Only if the server is stateless. On 2025-era servers that keep sessions, initialize returns an
MCP-Session-Id header that every later request must carry. A monitor sends one request and
cannot pass a header from one answer to the next, so a bare tools/list gets a 400 from those
servers.
A stateless server answers tools/list without a handshake, and a check on it is a stronger test:
it proves the tool registry loads, not just the transport. Use the 2025-era body with
"method":"tools/list","params":{} and a keyword such as the name of one tool you expect to be
there. Keep the body under 8 KB and make sure the full reply is not larger than 1 MiB, or the
keyword may sit past the part Fettle reads.
For session-keeping servers, the initialize probe has a side effect worth knowing: each check
opens a session the monitor never closes. At a five-minute interval that is 288 a day for each
region, so make sure your server expires idle sessions.
How do you monitor an MCP server behind authentication?
Add the credential as a header, usually Authorization: Bearer …, using a token issued for
monitoring only. Fettle stores request headers and never returns their values: reading the
monitor back shows a mask instead of the token. Without a credential, a protected server answers
401 and the check fails.
"request_headers": {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
"Authorization": "Bearer YOUR_MONITORING_TOKEN"
}
Servers that accept only OAuth tokens are harder, because those tokens expire and a monitor cannot
refresh one. Check whether the server lets you mint a long-lived service token or API key for
monitoring. If it does not, you have two weaker fallbacks. Some servers answer certain calls
anonymously; Fettle's own endpoint, for instance, answers tools/list and ping without a
credential but returns 401 with a WWW-Authenticate header to an anonymous initialize. Or set
expected_status_codes to [401] with no keyword: that proves the server is up and rejecting
strangers, nothing more.
How do you monitor a stdio MCP server?
With a heartbeat. A stdio server runs as a child process of the client, so there is no URL to probe. Instead, start it on a schedule from cron or CI, ask it for its tools, and ping a Fettle heartbeat when that works. If the ping stops coming or the job reports a failure, you get an alert.
The MCP Inspector CLI does the client half: it launches the server, runs one method and exits non-zero if anything fails. A wrapper script for cron:
#!/bin/sh
# Smoke test for a stdio MCP server; run it every hour from cron.
PING=https://api.fettle.sh/v1/heartbeat/YOUR_TOKEN
if npx -y @modelcontextprotocol/inspector --cli -e API_KEY="$API_KEY" \
node /srv/my-mcp-server/build/index.js --method tools/list > /dev/null; then
curl -fsS --retry 3 "$PING" > /dev/null
else
curl -fsS --retry 3 "$PING/fail" > /dev/null
fi
The Inspector starts the server with a minimal environment, so pass each variable it needs with
-e. Create the heartbeat with a period that matches the schedule (3600 seconds for hourly) and a
grace period that covers a slow run. A ping to /fail marks it down at once; silence raises the
alert once the period and grace run out. A heartbeat is never late before its first successful
ping, so a job that never ran at all stays pending rather than down. Free includes
3 heartbeats; the details are in the heartbeat docs.
The same script works as a scheduled CI job, which also catches a broken release of a server you
publish to npm: point the command at npx -y your-package instead of a local build. The
GitHub Actions cron guide shows the workflow side.
The Inspector CLI can also reach a remote server (--cli https://mcp.example.com/mcp --method tools/list), which is the route for a server whose transport the HTTP check cannot follow.
If you run Fettle from Claude Code or Codex, the agent can set all of this up for you: see uptime monitoring from Claude Code or Codex. More guides are under guides.
Questions
- Is a GET request to /mcp enough to know an MCP server is up?
- No. A GET only proves the web server answers. MCP runs over JSON-RPC in POST requests, and many servers return HTTP 200 even when the request failed, with the error inside the body. Send a real JSON-RPC request and require a field that only a working answer contains.
- Can an uptime monitor call tools/list on an MCP server?
- Only on a stateless server. A server that keeps sessions expects an initialize request first and a session header on everything after it, which a single-request monitor cannot carry. Such servers usually answer a bare tools/list with HTTP 400, so probe initialize or server/discover instead.
- How do you monitor an MCP server that only runs over stdio?
- A stdio server has no URL to probe, so run a smoke test on a schedule instead: start the server with the MCP Inspector CLI, list its tools, and ping a heartbeat URL when that succeeds or its /fail URL when it does not. Silence or a failure ping raises the alert.
Start monitoring in a minute
The free plan checks up to 20 monitors as often as every 3 minutes from 1 region, and emails you when one breaks. See pricing for the paid intervals, or follow the quickstart to do it from the API.