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.

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.
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.
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:
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.
The second pillar is metrics — aggregate numbers that answer trend questions. Three must be installed:
tools/call, resources/read). This measures load and can be an early alarm for strange spikes.An example of a simple counter with the OTel Metrics API:
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.
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:
{
"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.
The three pillars don't stand alone. The debugging flow you should make a habit of:
send_email error rate rising on a dashboard.tools/call trace, see which span deviates.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.
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:
mcp.tool attribute, and don't close streaming spans too early.requestId to connect logs, traces, and user tickets.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!