Migration Guide
Migrating an MCP server from raw @opentelemetry/api instrumentation to opentel-mcp — what stays, what gets deleted, before/after code.
If you've already hand-instrumented an MCP server with raw
@opentelemetry/api — manually calling tracer.startActiveSpan()
around each tool handler — migrating to opentel-mcp means deleting that
manual span code and replacing it with one instrumentMcpServer() call.
Your existing TracerProvider setup doesn't change.
TL;DR
Delete your manual startActiveSpan()/span.end() wrapping around
tool handlers. Call instrumentMcpServer(server, {}) once, before
registering tools — no setupNodeSdk, no serviceName, since your
TracerProvider is already registered. opentel-mcp picks it up as-is,
and adds CallToolResult.isError detection your manual instrumentation
almost certainly didn't have.
What does hand-rolled @opentelemetry/api instrumentation typically look like?
Wrapping each tool handler manually, something like this:
import { trace, SpanStatusCode } from '@opentelemetry/api';
const tracer = trace.getTracer('my-mcp-server');
server.setRequestHandler(CallToolRequestSchema, async (request) => {
return tracer.startActiveSpan(`tools/call ${request.params.name}`, async (span) => {
try {
const result = await originalHandler(request);
span.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (err) {
span.recordException(err);
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
throw err;
} finally {
span.end();
}
});
});This catches thrown exceptions. It does not catch a tool that returns
normally with CallToolResult.isError: true — see
Silent Failures for why that's a real gap, not
a hypothetical one. Nothing in raw @opentelemetry/api knows MCP's
CallToolResult shape; it only sees whatever span code you wrote by
hand.
What do I replace it with?
import { instrumentMcpServer } from 'opentel-mcp';
// Your TracerProvider is already registered elsewhere — no changes there.
instrumentMcpServer(server, {});Delete the manual startActiveSpan() wrapping entirely. Since
setupNodeSdk defaults to false, opentel-mcp emits spans through
whatever TracerProvider your application already registered globally —
it never creates or overrides one. serviceName isn't needed either;
that's only for opentel-mcp's own NodeTracerProvider, created only when
setupNodeSdk: true. See API Reference for the
full InstrumentOptions list.
What do I gain that my manual instrumentation didn't have?
CallToolResult.isErrordetection. The manual example above marks a spanERRORonly on a thrown exception. opentel-mcp also marks itERRORwhen a handler returns normally withisError: true— see Silent Failures.- Four
mcp.tool.*metrics (calls,errors,silent_failures,duration) for free, once you register aMeterProvider— see Metrics. Manual instrumentation usually only had spans, not metrics, since wiring up counters and histograms by hand for every handler is enough extra code that it tends not to happen. - Deep Failure Fingerprinting, on by default — a stable identifier per underlying failure, so the same bug doesn't look like a hundred different errors across a hundred spans.
What do I need to double-check after migrating?
- Call order.
instrumentMcpServer()must run before any tool handler is registered, same constraint as a fresh install — see Getting Started. - Span name and attributes will change. opentel-mcp names spans
{mcp.method.name} {tool name}(e.g.tools/call echo) and sets a specific attribute set (mcp.method.name,gen_ai.tool.name,gen_ai.operation.name,mcp.tool.argument_count,jsonrpc.request.id) — see API Reference. If a dashboard or alert was built against your old manual span names or attributes, it needs updating. - Don't double-instrument. If your manual wrapping and
instrumentMcpServer()are both active on the same handler, you'll get nested or duplicate spans. Remove the manual wrapping as part of the same change, not after.
Where do I go from here?
- Silent Failures — the detection gap this migration closes.
- Metrics — wiring up a
MeterProviderto get the fourmcp.tool.*instruments. - API Reference — full
instrumentMcpServer()signature and options.