Observability for a normal web service asks "is it up, and is it fast?" Observability for an MCP server has to ask a third question: "is the contract it's exposing to AI clients still the one they last saw?" Miss that third question and everything on your dashboard can look green while agents calling your tools quietly start failing.
The three things worth watching
MCP observability breaks down into logs, metrics, and protocol-level checks. Logs tell you what happened for a specific call. Metrics tell you the shape of behavior over time. Protocol-level checks tell you whether the server is honoring the contract it advertises — independent of whether any client happened to call it recently.
What to put in your logs
Structured logging is non-negotiable here, because you'll want to query these fields later, not grep them. A useful log line for a tool call captures:
{
"ts": "2026-07-29T14:02:11Z",
"server": "weather-mcp",
"tool": "get_forecast",
"caller": "agent-42",
"protocol_version": "2026-07-28",
"duration_ms": 184,
"jsonrpc_error": null
}
A few things worth calling out. The caller field matters more than it used to: the 2026-07-28 spec removed protocol-level sessions (no more Mcp-Session-Id), so if you need to correlate calls from the same agent across requests, you now need an explicit application-level handle rather than relying on a session the transport hands you. And protocol_version earns its own field because, for a while yet, you'll have some clients still negotiating the older initialize-based handshake and others using the newer server/discover flow — you want to know which one you're actually seeing traffic from.
Metrics that actually predict trouble
Raw request counts are the least useful metric you can track. What matters more:
- Latency percentiles, not averages. State your target at P95 — "95% of tool-call latencies under 600ms" — with P50 as headline reassurance and P99 as your early-warning canary for architectural problems. End-to-end latency for a tool call is the sum of model latency, MCP round-trip latency, your handler's own latency, and any downstream API it calls — instrument each segment separately if you can.
- Error rate by type, not just count. A JSON-RPC response can return HTTP 200 and still carry an
errorobject in the body. Bucket errors by JSON-RPC code and by which method they came from; a spike in errors fromtools/callon one specific tool is a very different problem from a spike in transport-level timeouts. - Tool and resource popularity. Which tools actually get called, by whom, and how often. This tells you where to spend reliability effort and where token/cost usage is concentrated — MCP tool calls can burn tokens fast, and cost tracking per tool is worth a dashboard of its own.
- Cache behavior. The 2026-07-28 spec adds
ttlMsandcacheScopemetadata totools/listresponses. If your clients are honoring that, you should be tracking how often they're serving cached tool lists versus fetching fresh ones — a mismatch here is a common source of "but I changed the schema and nothing broke" surprises.
Protocol-level checks are not the same as uptime
A generic uptime monitor confirms a port is open or an HTTP endpoint returns 200. Neither confirms your server can actually complete an MCP exchange. A protocol-level check does the real thing: negotiates capabilities the way a client would, then calls tools/list and validates the shape of what comes back.
The check that catches the most real incidents isn't availability at all — it's a diff. Store the last known-good tools/list response, hash or structurally compare each new one against it, and alert specifically when a tool's name, required parameters, parameter types, or enum values change. MCP has no built-in versioning enforcement for tools, so a server can rename a parameter or quietly drop a tool and nothing in the protocol announces it. Watching for it is the only way to catch it before an agent does.
Alerting: separate "down" from "different"
Logs are not natively actionable for paging — you need metrics and thresholds behind an alert, not a promise to "check the logs later." Split your alerts into at least two categories: availability failures (handshake doesn't complete, requests time out, transport drops connections) and contract failures (the server answers fine, but the schema underneath it moved). The second category is the more dangerous one precisely because everything else on a standard dashboard looks fine.
This is the exact niche MCP Vitals runs in continuously: it performs the real handshake against your server, diffs tools/list against history, and separates availability alerts from schema-drift alerts so you're not stuck sifting through generic uptime noise for the change that actually matters.
Closing
Good MCP observability isn't a fancier dashboard — it's answering a question generic tooling was never built to ask: has the contract changed? If your current setup can't tell you that without you manually re-reading a schema, it's worth closing that gap before an agent finds it for you.