Learn MCP - Observability & Logging
Series/Learning MCP/Episode 15
Episode 15 of 23

Learn MCP - Observability & Logging

This episode builds observability for an MCP server: replacing the deprecated Logging protocol with OpenTelemetry tracing for requests and streaming, metrics like call rate, error rate, and latency, plus structured logging with request IDs to trace a single request from start to finish.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Introduction

In episode 14 you hardened the MCP server with layered mitigations. But a fortress without scouts is blind: you won't know about attacks that were held back, slow tools, or errors piling up behind a single endpoint. Episode 15 installs those scouts — observability. The ability to answer three questions within five minutes when production is broken: what happened, which request is problematic, and why.

This episode's roadmap: the history of the deprecated Logging protocol and its replacement, OpenTelemetry tracing for requests and streaming, call rate, error rate, and latency metrics, then structured logging with request IDs that ties it all together.

From the Logging Protocol to OpenTelemetry

The early MCP spec provided Logging as a built-in method — servers send log levels to the client. The problem: every host implemented its own log storage and format, so observability was fragmented and non-standardized. In the modern spec (2026-07-28), the Logging protocol is officially deprecated with a removal clock of around one year — and its replacement isn't a new method, but OpenTelemetry (OTel), a mature industry standard.

Why OTel wins: it separates instrumentation (in code) from the backend (where data is sent), supports the three pillars — traces, metrics, logs — in one ecosystem, and already has SDKs and exporters for almost every language. Your MCP server just needs to produce standard telemetry; anyone can choose their own backend (Jaeger, Grafana Tempo, Prometheus, or a SaaS service) without touching the code.

Tracing with OpenTelemetry

The first pillar is tracing. Every incoming JSON-RPC request becomes a span — a time slice with a name, duration, and attributes. A single tools/call request can form a span tree: an HTTP span for the transport, a tool execution span, and an outgoing request span triggered by that tool.

Instrumenting an MCP server with the OTel SDK looks like this:

server.ts - span untuk tiap request JSON-RPC
import { trace } from "@opentelemetry/api";
import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
 
const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({
    url: "http://otel-collector:4318/v1/traces"
  }),
  serviceName: "mcp-server"
});
await sdk.start();
 
async function handleCall(req) {
  const span = trace.getTracer("mcp-server").startSpan("tools/call", {
    attributes: {
      "rpc.method": req.method,
      "rpc.system": "jsonrpc",
      "mcp.tool": req.params.name
    }
  });
  try {
    return await executeTool(req.params);
  } finally {
    span.end();
  }
}

Note the mcp.tool attribute — this is what makes traces useful: you can search for all calls to a specific tool and see which ones are slow. The same practice applies to streaming: a span doesn't end when the response headers are sent, but when the last SSE event finishes or the connection closes. Otherwise, traces will show a request as "done" while streaming is only halfway through.

Metrics: Call Rate, Error Rate, Latency

The second pillar is metrics — aggregate numbers that answer trend questions. Three must be installed:

  • Call rate — how many requests per second per method (tools/call, resources/read). This measures load and can be an early alarm for strange spikes.
  • Error rate — the proportion of failed requests, broken down per tool and per JSON-RPC error code. An error rate spike almost always precedes user complaints.
  • Latency — request duration, usually stored as a histogram with percentiles (p50, p95, p99). For streaming requests, measure time-to-first-event and total duration separately.

An example of a simple counter with the OTel Metrics API:

metrics.ts - counter call dan error
import { metrics } from "@opentelemetry/api";
 
const meter = metrics.getMeter("mcp-server");
const calls = meter.createCounter("mcp.calls.total", {
  description: "Jumlah request JSON-RPC per method"
});
const errors = meter.createCounter("mcp.errors.total", {
  description: "Jumlah error per method"
});
 
calls.add(1, { "rpc.method": "tools/call" });
errors.add(1, { "rpc.method": "tools/call", "mcp.error_code": "-32603" });

An important rule: attribute decoration must not be high-cardinality. Using taskId or userId as metric attributes will blow up the series count in Prometheus. Per-request identity belongs in traces; metrics only hold limited dimensions like method, tool, and error code.

Structured Logging & Request IDs

The third pillar — logs — is the last safety net when traces aren't captured. Good logging in the modern era is structured: one JSON line per event, not free-form text that's hard to parse. An ideal request log line looks like:

Satu baris log terstruktur
{
  "level": "error",
  "timestamp": "2026-08-03T07:12:04.582Z",
  "service": "mcp-server",
  "requestId": "req_9f2c71a4b8",
  "method": "tools/call",
  "tool": "send_email",
  "error": "upstream timeout after 5000ms",
  "durationMs": 5120
}

Note the requestId in that line. These are request IDs — an identity created when a request comes in, forwarded to all downstream operations (tool calls, outgoing requests, worker tasks), and sent back to the client in the response header. With a request ID, you can stitch together one story: an error log on the server, a trace in Jaeger, and a user complaint in a ticket — with just one string.

Info

Connect the three pillars with an ID convention: use the same request ID as the trace_id in OTel (when possible) or at least store it in span attributes and in logs. Standardizing this ID is what turns observability from three silos into one timeline that can be retold.

Putting It All Together

The three pillars don't stand alone. The debugging flow you should make a habit of:

  1. Get a signal from metrics — for example the send_email error rate rising on a dashboard.
  2. Click into the failing tools/call trace, see which span deviates.
  3. Pull the structured logs for that request ID to see the raw error message.
  4. Fix it, deploy, and watch the metric drop back down.

For the stdio server from episode 13, remember the rule: telemetry and logs must not leak to stdout. Export OTel to a collector via HTTP inside the process, and write structured logs to stderr or a file — not to the protocol channel.

Conclusion

Episode 15 equipped your MCP server with eyes: the deprecated Logging protocol was replaced by OpenTelemetry, tracing connects tools/call requests all the way through streaming with attributes like mcp.tool, call rate, error rate, and latency metrics give trend signals, and structured logging with request IDs is the final net tying the whole story together.

Key takeaways:

  • The Logging protocol is deprecated — point your investment at OpenTelemetry as the replacement standard.
  • Tracing follows the request: a span per JSON-RPC request, with the mcp.tool attribute, and don't close streaming spans too early.
  • Three mandatory metrics: call rate, error rate, and latency with a percentile histogram.
  • Logs must be structured JSON and every line carries a requestId to connect logs, traces, and user tickets.
  • Keep attribute cardinality low in metrics — per-request identity goes into traces, not metrics.

In the next episode 16 we widen the view from one server to many: Proxy, Gateway & Fleet — building an MCP gateway for many servers, unified auth, routing, and fleet management with versioning and rolling deployment. See you there!