Introduction
A production MCP server that cannot tell you what it is doing is a liability. Structured logging, request metrics, and health checks transform your server from a black box into an observable system. This lesson covers the built-in logging primitives in MCP, the metrics you must track, and how to expose health endpoints for load balancers.
Key Concepts
- Structured Logging: MCP provides
server.sendLoggingMessage()for emitting structured log entries withlevel,logger, anddatafields. This replaces ad-hoc console output with machine-parseable, client-visible log messages. - Log Levels: MCP supports
debug,info,notice,warning,error,critical,alert, andemergencylevels. Use them deliberately:debugfor development traces,infofor normal operations,warningfor recoverable issues,errorfor failures, andcriticalfor system-level problems. - Request Metrics: Track three core metrics per tool: request count, latency percentiles (P50, P95, P99), and error rate. These tell you what is being called, how fast it responds, and how often it fails.
- Health Check Endpoints: For HTTP-transported servers behind load balancers, expose a
/healthendpoint that returns server status, uptime, and connection state. This allows infrastructure to route traffic away from unhealthy instances. - Request Tracing: Every MCP interaction flows through Host, Client, and Server. Assigning a trace ID at the host level and propagating it through each layer lets you reconstruct the full path of any request.
Real World Context
Your team deploys an MCP server that wraps a payment processing API. On Monday morning, the on-call engineer gets paged: tool invocations are timing out. Without structured logging, they SSH into the server and grep through unstructured stdout. With proper monitoring, they open Grafana, see that P99 latency spiked at 2:14 AM, correlate it with a downstream API deployment, and identify the root cause in minutes. The difference between these two scenarios is observability.
Deep Dive
Structured Logging with MCP
The MCP SDK provides a first-class logging mechanism that sends log messages to the connected client. This is distinct from writing to stderr because the client can display, filter, and forward these messages.
typescriptimport { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; const server = new McpServer( { name: "payments-server", version: "1.0.0" }, { capabilities: { logging: {}, tools: {} } } ); server.tool( "process_payment", "Process a payment transaction", { amount: z.number(), currency: z.string() }, async ({ amount, currency }) => { const start = performance.now(); server.sendLoggingMessage({ level: "info", logger: "tool-handler", data: { tool: "process_payment", amount, currency, status: "started" } }); try { const result = await callPaymentAPI(amount, currency); const duration = performance.now() - start; server.sendLoggingMessage({ level: "info", logger: "tool-handler", data: { tool: "process_payment", duration_ms: Math.round(duration), status: "success", transactionId: result.id } }); return { content: [{ type: "text", text: `Payment processed: ${result.id}` }] }; } catch (error) { const duration = performance.now() - start; server.sendLoggingMessage({ level: "error", logger: "tool-handler", data: { tool: "process_payment", duration_ms: Math.round(duration), status: "error", error: error.message } }); return { content: [{ type: "text", text: `Payment failed: ${error.message}` }], isError: true }; } } );
Every log entry includes the tool name, duration, and status. This makes it trivial to query logs for all failed invocations or all calls exceeding a latency threshold.
Core Metrics to Track
Three metrics give you comprehensive visibility into server health:
textMetric What It Tells You ───────────────────────────────────────────────────── Request count Traffic volume per tool Latency percentiles P50 (typical), P95, P99 (worst case) Error rate per tool Reliability of each tool
Implement a lightweight metrics collector that aggregates these values in memory and exposes them via a health endpoint or pushes them to a metrics backend.
Health Check Endpoint
For HTTP-transported MCP servers, add a health endpoint alongside the MCP endpoint:
typescriptimport express from "express"; const app = express(); const startTime = Date.now(); app.get("/health", (req, res) => { res.json({ status: "healthy", uptime_seconds: Math.floor((Date.now() - startTime) / 1000), server: "payments-server", version: "1.0.0", tools_registered: 3, metrics: { total_requests: metricsCollector.totalRequests, error_rate: metricsCollector.errorRate, avg_latency_ms: metricsCollector.avgLatency } }); });
Load balancers poll this endpoint to determine whether to route traffic to this instance. Include enough information for quick triage but not so much that the endpoint becomes expensive to compute.
Tracing Across the MCP Chain
An MCP request traverses three layers: Host (the application running the LLM), Client (the MCP client inside the host), and Server (your MCP server). To trace a request end-to-end, propagate a trace ID from the host through each layer. In HTTP transport, use standard headers like X-Trace-Id. In stdio transport, embed trace context in the JSON-RPC metadata.
Common Pitfalls
- Logging too much at info level — High-volume debug information logged at info level creates noise that obscures real issues. Reserve info for meaningful state transitions and use debug for verbose traces.
- Ignoring latency percentiles — Average latency hides outliers. A server with 50ms average latency might have P99 at 5 seconds, meaning one in a hundred requests is painfully slow. Always track percentiles.
- Health endpoints that lie — A health check that returns 200 OK when the server cannot reach its downstream dependencies gives false confidence. Include dependency checks in your health endpoint.
Best Practices
- Use structured data in every log entry — Include tool name, duration, status, and a correlation ID in every log message. This makes logs queryable and filterable without regex gymnastics.
- Separate operational logs from protocol logs — MCP protocol-level messages (handshakes, capability negotiation) should be logged at debug level. Tool invocation outcomes should be logged at info or error level.
- Set up baseline metrics before you need them — Instrument your server on day one. When an incident happens, you need historical data to identify when behavior changed.
Summary
- Use
server.sendLoggingMessage()with structured data for machine-parseable, client-visible logging. - Track three core metrics per tool: request count, latency percentiles (P50/P95/P99), and error rate.
- Expose health check endpoints for load balancers with server status, uptime, and dependency health.
- Propagate trace IDs across the Host-Client-Server chain for end-to-end request tracing.
- Choose log levels deliberately: debug for traces, info for operations, error for failures, critical for system-level problems.
Code Examples
server.sendLoggingMessage({
level: "info",
logger: "tool-handler",
data: {
tool: "query_db",
duration_ms: 142,
status: "success",
rows_returned: 37
}
});