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 with level, logger, and data fields. This replaces ad-hoc console output with machine-parseable, client-visible log messages.
  • Log Levels: MCP supports debug, info, notice, warning, error, critical, alert, and emergency levels. Use them deliberately: debug for development traces, info for normal operations, warning for recoverable issues, error for failures, and critical for 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 /health endpoint 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.

typescript
import { 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:

text
Metric                  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:

typescript
import 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

  1. 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.
  2. 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.
  3. 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

  1. 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.
  2. 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.
  3. 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

typescript
server.sendLoggingMessage({
  level: "info",
  logger: "tool-handler",
  data: {
    tool: "query_db",
    duration_ms: 142,
    status: "success",
    rows_returned: 37
  }
});
✓ Completed