The problem: your MCP server is a black box the moment it leaves your laptop
Here's a scenario that plays out for almost every team building agents: an autonomous workflow runs overnight, a few tool calls succeed, one fails silently, and the agent reports something vague like "I wasn't able to retrieve that." No stack trace, no tool name, no timing. You're left guessing which of your five MCP servers dropped the ball.
This isn't a hypothetical edge case — it's the default experience once you move past mcp dev and local testing. The server side of MCP is where visibility disappears first: a slow tool call, a tools/list response that quietly grew to dozens of entries, a downstream API returning a 500 wrapped in a polite isError result — none of it shows up unless you deliberately put it there.
There's also a spec-level wrinkle worth knowing about if you've been relying on MCP's built-in logging capability: the protocol has been moving away from protocol-level logging as a first-class feature, which means you shouldn't build your production debugging story around it. The safer bet is to own your logging and tracing at the application layer, independent of what the spec guarantees.
This post walks through a setup you can add to an existing MCP server in an afternoon — no observability vendor, no OpenTelemetry collector cluster, just structured logs and a correlation ID pattern that will save you hours the next time something breaks at 2am.
Why standard API debugging tools don't apply cleanly
If you've operated a REST API before, you already have instincts for this: instrument the HTTP layer, log request/response pairs, ship to your logging stack, query when something breaks. MCP breaks those instincts in a few specific ways:
- Protocol wrapping. Tool calls happen over JSON-RPC or HTTP, but a single tool invocation can chain multiple operations inside the server. One "call" in the trace might represent five internal steps.
- Session state. MCP servers often maintain state across a session, so observability needs to capture state transitions, not just discrete request/response pairs.
-
Compound side effects. A tool that loops — say, calling
create_issuerepeatedly due to an agent retry bug — doesn't fail loudly. It just creates duplicate issues. "Did the call succeed" is the wrong question; "how many downstream effects occurred, and are they recoverable" is the right one. - Credential opacity. If your server supports multiple auth modes (service account, bring-your-own-token, delegated), your logs need to capture which identity actually executed a given call, not just that a call happened.
None of this is exotic engineering. It just means the logging you'd bolt onto a typical REST service isn't quite enough here.
Step 1: Give every request a correlation ID, end to end
The single highest-leverage change you can make is attaching a request ID the moment a tool call enters your server, and threading it through every log line, downstream API call, and error message tied to that request.
import contextvars
import uuid
request_id_var = contextvars.ContextVar("request_id", default="")
async def handle_tool_call(request):
request_id = str(uuid.uuid4())
token = request_id_var.set(request_id)
try:
# ... your existing tool dispatch logic
pass
finally:
request_id_var.reset(token)
A few implementation traps to watch for:
- Don't rely on a server-level "on request" decorator that may not exist in your framework version — inject the ID directly at the top of your tool dispatch function or via a transport-layer wrapper instead.
- If you're running concurrent coroutines and logging the same ambient request ID from both, you'll get log lines that look correlated but aren't — scope the context var narrowly to a single tool invocation, not a whole session.
- When you serialize the ID into JSON logs, guard against
Noneexplicitly (request_id_var.get() or ""), otherwise a default value can quietly break downstream log parsers that expect a string.
Step 2: Log tool calls as structured events, not print statements
print() statements don't survive process or subprocess boundaries cleanly, and they make your logs unsearchable. Instead, log every tool call as a single structured JSON event with a consistent shape:
{
"timestamp": "2026-09-25T14:02:11Z",
"request_id": "a1b2c3d4",
"tool": "create_issue",
"agent_id": "agent-42",
"args_hash": "sha256:...",
"duration_ms": 812,
"status": "error",
"error_code": "UPSTREAM_500",
"auth_mode": "server-managed"
}
Two design choices matter more than they seem:
- Hash or redact arguments instead of logging them raw. MCP tools frequently pass user data, tokens, or business-sensitive payloads. Avoid collecting authentication data, credentials, and personally identifiable information in your logs — if you need to correlate calls back to a user later, use the correlation ID plus a lookup table you control, not raw PII in the log line itself.
- Log the auth mode that actually fired. If your server supports more than one credential path, the calling agent often has no idea which one executed. Capturing that explicitly is the difference between a five-minute incident review and a half-day one.
Step 3: Set latency baselines per tool, not a universal number
Don't reach for an industry-standard latency table — there isn't a meaningful one. A tool that calls an external API has a fundamentally different latency floor than one that reads from local memory, and initialize/tools/list calls should be near-instant regardless. The practical approach:
- Capture p50/p95/p99 latency per tool method over a few days of normal traffic to establish your own baseline.
- Alert on regressions against that baseline, not an arbitrary fixed threshold.
- Treat a sudden spike in
tools/listsize or a previously-fast tool crossing its p95 as an early warning sign, not just outright errors.
Step 4: Distinguish "it failed" from "it lied"
The most expensive failure mode in agent systems isn't the loud error — it's the tool call that returns a technically well-formed response that's wrong. The server logs a success. The agent moves on confidently. Nobody notices for a week. Structured logging alone won't catch this; you need a lightweight assertion layer that checks tool outputs against expected shapes or value ranges before treating a call as "successful," and logs a distinct status: "suspicious" event when something looks off. This is a smaller cousin of the eval-style testing you'd run in CI, applied at runtime.
Where this fits with the rest of your production checklist
Logging and tracing are one piece of a larger production-readiness picture — alongside session handling, auth scoping, error recovery, and test coverage for tool behavior. If you're setting this up for the first time and want a single reference to work from rather than piecing it together from a dozen blog posts, the MCP Production Pack bundles a production checklist, a minimal working server template with these logging patterns already wired in, and agent-eval test templates you can adapt to catch the "confident but wrong" failure mode described above.
Takeaways
- Don't depend on protocol-level logging guarantees — own structured logging at the application layer.
- Thread a correlation ID through every tool call, scoped narrowly to avoid concurrency bugs.
- Log tool calls as structured JSON events with hashed/redacted arguments, never raw credentials or PII.
- Build latency baselines per tool from your own traffic, not generic thresholds.
- Add a lightweight runtime check for "successful but wrong" responses — this is often the costliest failure mode and the hardest to see in raw logs.
Small, deliberate investment here pays off the first time you have to explain to a teammate exactly which tool call failed, why, and what it touched before it did.
Written with AI assistance and reviewed for accuracy.
Top comments (0)