# Monitor an MCP Server

Cronitor uptime checks can monitor a network-accessible Model Context Protocol (MCP) server by sending a safe JSON-RPC request and asserting on the response. This verifies more than an open port: it can prove that protocol routing, authorization middleware, and the tool-list endpoint are responding.

## Check reachability first

Cronitor checks originate from public probe locations. Before creating a check, confirm that the target has public DNS and is reachable from outside your laptop, VPC, VPN, or private LAN.

Choose the pattern that matches the target:

* **Public HTTPS endpoint:** check it directly. Require TLS and expose only the smallest protocol surface you need.
* **Public endpoint restricted by firewall:** enable Cronitor's fixed-IP pool in account settings and allowlist the addresses published at [cronitor.io/ips.txt](https://cronitor.io/ips.txt). Leave the check's regions unset to use your account defaults.
* **Private endpoint that may be tunneled safely:** publish only a health or protocol endpoint through your normal authenticated ingress.
* **Endpoint that must remain private:** do not open it for a probe. Run the request inside the private network and report the result with a heartbeat or job monitor. See [Monitoring Private Endpoints and APIs](https://cronitor.io/guides/monitor-private-endpoints.md).

Checks cannot probe a stdio MCP server because it has no network endpoint. Monitor the process that launches it with a heartbeat or job monitor instead.

The examples below use the Monitor API and create or update the same monitor key when rerun. Replace every `*.example.com` value. Copy your existing SDK Integration key from [API Settings](https://cronitor.io/app/settings/api), but never put the secret directly in a command or shell history. The non-echoing prompt below is for a temporary Bash or Zsh session (`read -s` is not portable POSIX shell syntax).

## Probe an MCP server with `tools/list`

An HTTP MCP server should do more than accept a TCP connection. Send a JSON-RPC `tools/list` request and assert that the response is successful, uses JSON-RPC 2.0, and contains the tool list.

MCP revision `2026-07-28` uses a stateless core: each request carries the protocol version and client capabilities in the body's `_meta`, with matching `MCP-Protocol-Version` and `Mcp-Method` headers. Use the recipe below only for a server whose documentation explicitly says it supports MCP `2026-07-28` and this stateless request shape. Do not infer support from the presence of an HTTP MCP endpoint. Legacy servers can reject `tools/list` before `initialize`.

```bash
printf 'Paste the SDK Integration key: ' >&2
IFS= read -r -s CRONITOR_API_KEY
printf '\n' >&2
export CRONITOR_API_KEY
curl --fail-with-body --silent --show-error \
  --user "$CRONITOR_API_KEY:" \
  --header 'Content-Type: application/json' \
  --header 'Cronitor-Version: 2025-11-28' \
  --request PUT \
  --data '{
    "monitors": [{
      "type": "check",
      "key": "mcp-tools-list",
      "name": "MCP tools/list",
      "schedules": ["every 5 minutes"],
      "request": {
        "url": "https://mcp.example.com/mcp",
        "method": "POST",
        "headers": {
          "Accept": "application/json, text/event-stream",
          "Content-Type": "application/json",
          "MCP-Protocol-Version": "2026-07-28",
          "Mcp-Method": "tools/list"
        },
        "body": "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\",\"params\":{\"_meta\":{\"io.modelcontextprotocol/protocolVersion\":\"2026-07-28\",\"io.modelcontextprotocol/clientInfo\":{\"name\":\"cronitor-uptime-check\",\"version\":\"1.0.0\"},\"io.modelcontextprotocol/clientCapabilities\":{}}}}"
      },
      "assertions": [
        "response.code = 200",
        "response.body contains jsonrpc",
        "response.body contains tools"
      ]
    }]
  }' \
  https://cronitor.io/api/monitors
unset CRONITOR_API_KEY
```

This is an ordinary HTTP check. The same request, headers, body, and assertions can be entered in the app's check form, and the monitor is labeled with the `http` platform.

The key is read from the environment and never appears in the command itself. Run `unset CRONITOR_API_KEY` after the request, including if you interrupt the example before its final line. The JSON assertion value is intentionally unquoted inside the assertion string. Cronitor compares it with the decoded JSON value.

If the endpoint returns `text/event-stream`, Cronitor's JSON assertions cannot inspect the SSE envelope. Keep the response-code assertion, replace the `response.json` assertion with `response.body contains jsonrpc`, and use body assertions or a dedicated JSON health endpoint for deeper validation.

### Probe a legacy MCP server with `initialize` only when explicitly safe

Do not use a recurring raw `initialize` request as a general legacy-server health check. A legacy Streamable HTTP server may create an `Mcp-Session-Id` for the response. A conforming client may then need to retain that identifier, send an `initialized` notification, include the identifier on later requests, and explicitly delete the session when finished. Cronitor uptime requests do not retain protocol state between runs, so a repeated `initialize` probe can leave abandoned sessions on the target.

Prefer a dedicated health endpoint for a legacy server. For an authorization-protected endpoint, you can instead verify the unauthenticated HTTP boundary returns the documented `401` response, without starting the MCP lifecycle.

Use `initialize` only when the server's own documentation explicitly guarantees that the request is sessionless, or that any session has a short bounded expiry and requires no client cleanup. Build that exceptional probe from the documented protocol version and lifecycle behavior; do not copy the modern stateless payload above into a legacy request.

### Add authentication for a protected MCP server

Use a dedicated, least-privilege service token that remains valid for unattended probes. Store and rotate it with your other service credentials, and add it to the request headers in the `tools/list` check:

```json
"Authorization": "Bearer YOUR_MCP_SERVICE_TOKEN"
```

Cronitor sends the header only to the URL configured for that check. Do not place secrets in the URL, monitor name, assertions, or tags.

### Treat `401 Unauthorized` as proof of life

You can verify that an OAuth-protected MCP endpoint, DNS, TLS termination, routing, and authorization middleware are alive without storing a token. Send the same `tools/list` request without credentials and replace the success assertions with:

```json
"assertions": ["response.code = 401"]
```

A passing `401` check proves only that the protected HTTP boundary responds. It does not prove that a token can authenticate or that the server can return its tool list. Run a separate authenticated check when you need that stronger guarantee.

## Alert before TLS expires

Every HTTPS check verifies the certificate by default. Add an explicit expiry assertion when you want warning before an MCP server certificate reaches its renewal window:

```text
ssl_certificate.expires_in > 30 days
```

Choose a threshold longer than your certificate renewal and incident-response window. Keep SSL verification enabled; an expiry warning is not useful if the check accepts an invalid certificate.

## Verify the check end to end

After creating the monitor, open it in Cronitor and run a test against the real target. Confirm the request method, headers, and body are safe to repeat, then test the alert path:

1. Point a staging copy at a closed port or temporary failing endpoint and confirm Cronitor opens an issue.
2. Restore the target and confirm the monitor recovers.
3. For a protected server, test the unauthenticated `401` boundary and authenticated `tools/list` checks separately.
4. Attach the intended notification list before relying on the monitor in production.

To have a compatible client create this check for you, connect it to the [Cronitor MCP server](https://cronitor.io/docs/mcp-server.md) and use the `setup_monitor` tool as a normal check: pass the `url`, `method` (`POST`), `headers`, `body`, and `assertions` from the example above.
