
Security News
When Autonomous Agents Escape: Why Socket Signed the Cyber Defense Open Letter
Socket joins more than 100 technology, cybersecurity, and financial organizations calling for a global surge in cyber defense.
W3C Trace Context bridge for the Model Context Protocol — propagate traces through MCP _meta and emit OpenTelemetry spans so Host -> MCP server -> tool -> downstream shows up as one trace.
Part of the StudioMeyer MCP Stack — Built in Mallorca 🌴 · ⭐ if you use it
W3C Trace Context bridge for the Model Context Protocol. It propagates trace context through MCP's _meta field and emits OpenTelemetry spans, so a request flowing Host → MCP server → tool → downstream HTTP shows up as one connected trace in Jaeger, Tempo, Honeycomb, or Datadog.
The MCP release candidate of 2026-07-28 nailed down distributed tracing for MCP:
_meta keys traceparent, tracestate, and baggage for W3C Trace Context and W3C Baggage. These keys ride along in params._meta of every request, and MCP transports must pass them through untouched.Spec post: https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/
So the protocol now says where the trace context lives and which telemetry system to use — but there was no small library that does the actual plumbing: read the caller's context out of _meta, start a correctly-parented OpenTelemetry span for the tool call, and let your downstream calls hang off it. That's all mcp-otel is.
It is deliberately thin. It does not configure OpenTelemetry for you, ship an exporter, or hide your tracer. You keep full control of sampling, resources, and exporters; mcp-otel only bridges _meta ↔ spans.
npm install mcp-otel @opentelemetry/api
# @modelcontextprotocol/sdk is an optional peer — only needed if you use the
# tool-handler wrapper against the SDK (the low-level helpers don't import it).
Peer dependencies:
| Package | Range | Required? |
|---|---|---|
@opentelemetry/api | ^1.9.0 | yes |
@modelcontextprotocol/sdk | >=1.10.0 <2 | optional (only for instrumentToolHandler's extra shape) |
Node 20+. ESM and CommonJS are both shipped.
Set OpenTelemetry up once at startup (this is your normal OTel bootstrap — mcp-otel doesn't do it for you), then wrap each tool handler.
import { instrumentToolHandler } from "mcp-otel";
import { z } from "zod";
server.registerTool(
"weather.lookup",
{ description: "Current weather", inputSchema: { city: z.string() } },
// The only change: wrap the handler. Same (args, extra) => result signature.
instrumentToolHandler("weather.lookup", async ({ city }) => {
// trace.getActiveSpan() here is the MCP server span.
// Any span you start, or any downstream fetch that injects the active
// context, nests under it automatically.
return { content: [{ type: "text", text: `Weather for ${city}` }] };
}),
);
instrumentToolHandler:
traceparent / tracestate / baggage from the caller's _meta
(it checks extra._meta — the published SDK 1.x shape — and extra.mcpReq._meta),SERVER span named tools/call <toolName> as a child of the caller's span,OK / ERROR status and records thrown exceptions.Attributes set on the span: mcp.method ("tools/call"), mcp.tool.name, mcp.request.id, and mcp.session.id when available. Pass attributes for GenAI conventions like gen_ai.system.
Prefer the explicit primitive? Use runInToolSpan when you have the _meta object directly:
import { runInToolSpan } from "mcp-otel";
const result = await runInToolSpan(
meta, // request.params._meta
{ toolName: "weather.lookup", requestId },
async (span) => {
span.setAttribute("weather.city", city);
return doWork(city);
},
);
If you write an MCP client, inject your active trace context into the request _meta so the server can continue your trace:
import { injectTraceContext } from "mcp-otel";
const result = await client.callTool({
name: "weather.lookup",
arguments: { city: "Palma" },
_meta: injectTraceContext({}), // writes traceparent/tracestate/baggage from the active context
});
injectTraceContext(meta, context?) mutates and returns the _meta object, defaulting to the current active OpenTelemetry context. Existing _meta keys (like progressToken) are preserved.
mcp-otel emits standard OpenTelemetry spans, so any exporter works. Minimal Jaeger setup:
docker run -d --name jaeger \
-p 16686:16686 -p 4318:4318 \
jaegertracing/all-in-one
// otel-setup.ts — import this once, before anything else.
import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { resourceFromAttributes } from "@opentelemetry/resources";
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
const sdk = new NodeSDK({
resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: "my-mcp-server" }),
traceExporter: new OTLPTraceExporter({ url: "http://localhost:4318/v1/traces" }),
});
sdk.start();
NodeSDK installs the AsyncLocalStorageContextManager that lets mcp-otel propagate the active span to your downstream calls. Open http://localhost:16686 and you'll see host → tools/call <name> → downstream as one trace.
Note on context propagation: the auto-nesting of downstream spans relies on a context manager being installed — exactly what
NodeSDK/NodeTracerProvider.register()does. In a process with no context manager (e.g. a bareBasicTracerProvider), spans are still created and correctly parented to the caller, butcontext.active()inside your handler won't carry the span. This is the standard OpenTelemetry contract, not a quirk of this library.
A zero-infrastructure example (in-memory exporter, prints the span tree) lives in examples/connected-trace.mjs:
npm run build
node examples/connected-trace.mjs
It prints:
- host.chat-turn [INTERNAL]
- tools/call weather.lookup [SERVER]
- GET /weather [CLIENT]
All 3 spans share one traceId: yes
The real-SDK wiring is in examples/mcp-server.ts.
instrumentToolHandler(toolName, handler, options?) → wrapped handler. Drop-in (args, extra) => result wrapper. Options: method (default "tools/call"), tracer, attributes, and getMeta / getRequestId / getSessionId extractor overrides.runInToolSpan(meta, options, body) → Promise<T>. Low-level primitive: runs body(span) inside a SERVER span parented to meta's trace context. Options: toolName, method, requestId, sessionId, attributes, tracer.extractTraceContext(meta, base?) → Context. Builds an OTel parent context from _meta (traceparent + tracestate + baggage). Defaults base to ROOT_CONTEXT for deterministic behavior.injectTraceContext(meta, context?) → Meta. Writes trace keys into _meta from context (default: active context). Mutates and returns meta.parseTraceparent(s) → SpanContext | null. W3C-conformant parse. Version 00 is length-strict (exactly four fields); a higher version is parsed forward-compatibly (first four fields read, trailing fields ignored, per the spec) so it keeps working as Trace Context evolves. Returns null for malformed, ff-version, or all-zero (invalid trace-id / parent-id) values. Result is isRemote: true.formatTraceparent(spanContext) → string | null. Serializes to a version-00 traceparent; null for an invalid span context.spanContextToContext(spanContext, base?) → Context. Wrap a span context you already hold into a context for injection.traceContextFields() → string[]. The _meta keys this library reads/writes (useful for stripping them before forwarding _meta).TRACEPARENT_META_KEY, TRACESTATE_META_KEY, BAGGAGE_META_KEY, TRACE_META_KEYS, and the attribute keys ATTR_MCP_METHOD, ATTR_MCP_TOOL_NAME, ATTR_MCP_REQUEST_ID, ATTR_MCP_SESSION_ID.
_meta and not HTTP headers?You might ask: MCP can run over Streamable HTTP, so why not just read traceparent from the HTTP request headers like a normal web service?
_meta means propagation works identically on every transport — which is exactly why the spec reserved these keys in _meta rather than leaning on HTTP._meta._meta travels with the logical request across those hops; a hop-by-hop HTTP header does not.So _meta is the only place that is both per-request and transport-independent — which is why SEP-414 standardized on it, and why this library reads/writes there.
A small family of focused, production-grade tools for building and operating MCP servers — mix and match:
mcp-fuzz (schema-aware fuzzer) + mcp-storm (load tester)ttlMs + cacheScope)MIT © StudioMeyer
FAQs
W3C Trace Context bridge for the Model Context Protocol — propagate traces through MCP _meta and emit OpenTelemetry spans so Host -> MCP server -> tool -> downstream shows up as one trace.
The npm package mcp-otel receives a total of 9 weekly downloads. As such, mcp-otel popularity was classified as not popular.
We found that mcp-otel demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
Socket joins more than 100 technology, cybersecurity, and financial organizations calling for a global surge in cyber defense.

Product
Enterprise security teams can now detect malware, credential theft, suspicious network activity, and risky updates across Microsoft Edge extensions.

Research
/Security News
Socket researchers found 18 Chrome extensions and one Edge extension delivering a wallet drainer, credential theft, and other malicious payloads.