AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
MCP verified MIT Self-run

Opentel Mcp

mcp-thirumalaiboobathi-opentel-mcp · by Thirumalaiboobathi

OpenTelemetry instrumentation for Model Context Protocol (MCP) servers

No reviews yet
0 installs
11 views
0.0% view→install

Install

$ agentstack add mcp-thirumalaiboobathi-opentel-mcp

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/mcp-thirumalaiboobathi-opentel-mcp)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
18d ago

Declared compatibility

Claude CodeClaude DesktopCursorWindsurf

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Opentel Mcp? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

opentel-mcp

> Turn every MCP tool call into an OpenTelemetry trace — including the > failures your logs won't show you.

[](https://github.com/Thirumalaiboobathi/opentel-mcp/actions/workflows/ci.yml) [](https://www.npmjs.com/package/opentel-mcp) [](https://www.npmjs.com/package/opentel-mcp) [](https://github.com/Thirumalaiboobathi/opentel-mcp/blob/main/LICENSE)

opentel-mcp watches every tool call your MCP (Model Context Protocol) server handles: which tool ran, how long it took, and whether it worked. It reports that as OpenTelemetry (OTel) traces — the standard most dashboards already read. One function call; no changes to your tools' code.

The problem

Your AI agent calls 15 MCP tools across 3 servers this turn. One tool returns { isError: true } inside an otherwise-successful response — how a tool reports "I couldn't do that" without crashing. Your logs show success. Your metrics show success. The agent gives a wrong answer, and nothing you're monitoring says why.

opentel-mcp makes that failure visible: one span per tool call, marked as an error when it actually is one, using the same standard your dashboards already speak.

Install

npm install opentel-mcp @opentelemetry/api

opentel-mcp is an ES module — add "type": "module" to package.json.

30-second quickstart

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { instrumentMcpServer } from 'opentel-mcp';
import { z } from 'zod';

const server = new McpServer({ name: 'my-server', version: '1.0.0' });

// Wraps every tool registered below. Must run BEFORE server.tool() —
// see "Ordering constraint" below for why.
instrumentMcpServer(server, {
  serviceName: 'my-mcp-server', // shows up on your traces
  setupNodeSdk: true, // dev mode: prints traces to your terminal
});

// A normal tool, registered exactly as usual.
server.tool('echo', { text: z.string() }, async ({ text }) => ({
  content: [{ type: 'text', text: `you said: ${text}` }],
}));

const transport = new StdioServerTransport();
await server.connect(transport);

That's it. Every tool call now emits a trace. Wire an exporter to see them (next section).

See it working

Run the snippet above and this prints to your terminal — a real, captured run (full dump: examples/hello-mcpserver/README.md):

name: 'tools/call echo'
kind: 1                    // SpanKind.SERVER
status: { code: 1 }        // OK
attributes: {
  'mcp.method.name': 'tools/call',
  'gen_ai.tool.name': 'echo',
  'mcp.tool.argument_count': 1,
  'jsonrpc.request.id': '1'
}

No dashboard needed — setupNodeSdk: true's dev exporter printed this directly. Point it at a real backend later; see "Two modes" below.


The rest of this README goes deeper: both server APIs, every attribute and metric emitted, how failure grouping works, configuration, and the non-obvious design decisions behind each.

Both server APIs

MCP servers are built on one of two classes from @modelcontextprotocol/sdk; opentel-mcp detects and wraps either one the same way (see ADR 001 in docs/adr/ for how).

McpServer — the high-level API most servers are actually built on. Use it unless you have a specific reason not to; this is what the quickstart above uses.

Server — the low-level API, for when you're handling raw JSON-RPC yourself or building a library on top of MCP:

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { instrumentMcpServer } from 'opentel-mcp';

const server = new Server({ name: 'my-server', version: '1.0.0' }, { capabilities: { tools: {} } });

// Must run before setRequestHandler(CallToolRequestSchema, ...) below.
instrumentMcpServer(server, { serviceName: 'my-mcp-server', setupNodeSdk: true });

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { text } = request.params.arguments ?? {};
  return { content: [{ type: 'text', text: `you said: ${text}` }] };
});

const transport = new StdioServerTransport();
await server.connect(transport);

Runnable versions of both live in examples/hello-server/ and examples/hello-mcpserver/.

What gets emitted

Tool-level failures, specifically

An MCP tool can fail two ways: it can throw, or it can return isError: true on an otherwise-successful response (the case from "The problem" above). opentel-mcp treats both the same way — span marked ERROR, nothing thrown, the result returned to the caller unchanged:

tools/call fetch_weather ................. 605ms   ERROR
error.type = tool_error

Verified in test/instrument.test.js's "tool-level failure" tests.

Span attributes

Every span follows the OpenTelemetry MCP semantic conventions (see "Semantic conventions" below), name {mcp.method.name} {tool name} (e.g. tools/call echo), kind SERVER, status ERROR whenever error.type is set.

| Attribute | Requirement Level | Description | Example | |---|---|---|---| | mcp.method.name | Required | JSON-RPC method name | "tools/call" | | genai.tool.name | Conditionally Required | Tool name from request | "echo" | | genai.operation.name | Recommended | GenAI operation type | "executetool" | | jsonrpc.request.id | Conditionally Required | JSON-RPC request id (string) | "abc-123" | | error.type | Conditionally Required (on failure) | Error class name, or "tool_error" when the tool call itself returned isError: true | "TypeError" | | mcp.tool.argumentcount | Custom — not spec | Number of arguments (values not captured) | 2 |

Span status description carries the error message on failure (thrown errors); there's no separate error-message attribute — the spec expresses success/failure through span status, not an attribute. Source of truth: src/attributes.js.

Metrics

Four mcp.tool.* metrics via @opentelemetry/api's Metrics API — same API-only pattern as tracing (see "Two modes" below): nothing is recorded until a MeterProvider is registered. Set enableMetrics: false to opt out even when one is; tracing is unaffected either way. Source of truth: src/metrics.js.

| Metric | Type | Unit | Attributes | Emitted when | |---|---|---|---|---| | mcp.tool.calls | Counter | — | gen_ai.tool.name, mcp.method.name | Every tool call | | mcp.tool.errors | Counter | — | gen_ai.tool.name, error.type[^1] | Handler threw or rejected | | mcp.tool.silent_failures | Counter | — | gen_ai.tool.name[^1] | Result had isError: true | | mcp.tool.duration | Histogram | ms | gen_ai.tool.name, mcp.tool.outcome[^1] | Every call, completion |

[^1]: Also carries mcp.failure.category when fingerprinting finds one — see "Failure Fingerprinting" below.

mcp.tool.silent_failures increments from the exact same check that marks the span ERROR (isToolResultError() in src/instrument.js) — the detection logic isn't duplicated between traces and metrics.

Wiring a real MeterProvider/TracerProvider — a worked example against SigNoz's local OTLP endpoint:

import { metrics, trace } from '@opentelemetry/api';
import { MeterProvider, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
import { NodeTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-trace-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { resourceFromAttributes } from '@opentelemetry/resources';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { instrumentMcpServer } from 'opentel-mcp';

const resource = resourceFromAttributes({ 'service.name': 'my-mcp-server' });

const meterProvider = new MeterProvider({
  resource,
  readers: [
    new PeriodicExportingMetricReader({
      exporter: new OTLPMetricExporter({ url: 'http://localhost:4318/v1/metrics' }),
    }),
  ],
});
metrics.setGlobalMeterProvider(meterProvider);

const tracerProvider = new NodeTracerProvider({
  resource,
  spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter({ url: 'http://localhost:4318/v1/traces' }))],
});
tracerProvider.register();

const server = new Server({ name: 'my-server', version: '1.0.0' }, { capabilities: { tools: {} } });

// setupNodeSdk: false (default) — both providers above are already
// registered globally, so instrumentMcpServer() picks them up as-is.
instrumentMcpServer(server, {});

http://localhost:4318 is SigNoz's default local OTLP/HTTP (OpenTelemetry Protocol — the wire format traces/metrics travel over) endpoint; point it at your own collector in production. @opentelemetry/sdk-metrics and @opentelemetry/exporter-metrics-otlp-http are host-app dependencies — opentel-mcp doesn't bundle them (see package.json's peerDependencies). @opentelemetry/sdk-trace-node and @opentelemetry/exporter-trace-otlp-http are already runtime dependencies of opentel-mcp itself (its setupNodeSdk: true dev path uses them), so no extra install is needed for those two.

Failure Fingerprinting (v0.4.0+)

Groups logically identical failures under one stable identifier, even when the error message contains UUIDs, timestamps, or user IDs. Ten calls that fail the same way but each mention a different user ID show up as one issue, not ten.

Runs locally and synchronously over the error object already in hand — no network call, no third-party service. Every thrown error and every isError: true result gets one, automatically; disable with { fingerprinting: false } (default: enabled). Full algorithm: ADR 006 in docs/adr/.

| Attribute | Description | Example | |---|---|---| | mcp.failure.fingerprint | Stable 16-hex-char identity for the failure | "a3f4c8e2b1d09f77" | | mcp.failure.signature | Human-readable errorClass@fn:line, ≤60 chars | "TypeError@doThing:42" | | mcp.failure.category | One of 8 categories (below) | "timeout" | | mcp.failure.origin | tool_error \| thrown \| transport | "thrown" | | mcp.failure.error_class | Error class / constructor name | "TypeError" |

Source of truth: src/fingerprint/attributes.js. Every category:

  • validation — bad input (Zod/Joi/Yup errors, "invalid"/"required" wording)
  • timeout — an operation timed out (TimeoutError, ETIMEDOUT, ...)
  • network — a connection failed (ECONNREFUSED, FetchError, ...)
  • auth — 401/403, "unauthorized"/"forbidden" wording
  • dependency — a downstream service or package failed (Mongo, Postgres, ...)
  • serialization — malformed JSON, "unexpected token" wording
  • internal — nothing more specific matched (the catch-all)
  • unknown — fingerprinting itself hit an internal error (should not normally happen)

Full classifier source: src/fingerprint/classify/.

Cardinality: the fingerprint itself is unbounded — a new bug means a new fingerprint, forever. That's fine on span attributes (each span is its own record), but it must never go on a metric label, or every distinct failure becomes its own permanent time series. opentel-mcp enforces this structurally, not by convention: src/metrics.js can only reach a fingerprint-derived value through METRIC_SAFE_ATTRIBUTES — a frozen list containing only category and origin (24 combinations max). There is no code path today that could accidentally attach fingerprint, signature, or error_class to a counter or histogram label. See src/fingerprint/attributes.js and ADR 006's "Consequences" section.

Extending it: computeFingerprint(err, ctx, opts) (src/fingerprint/compose.js) accepts opts.classifiers to prepend your own detection rules ahead of the built-in eight, and opts.stackFrames to change how many stack frames feed the signature — see test/fingerprint/compose.test.js's "uses a custom classifiers list" and "respects a custom opts.stackFrames count" tests, and examples/fingerprint-demo.js for a runnable, standalone demo (node examples/fingerprint-demo.js). Not yet wired through instrumentMcpServer()'s own options — today this means importing computeFingerprint directly rather than configuring the automatic per-call-site wrapping; tracked in the roadmap below.

Configuration

All options passed to instrumentMcpServer(server, options). Source of truth: src/config.js.

| Option | Type | Default | Description | |---|---|---|---| | serviceName | string | — | Resource name for traces[^2] | | setupNodeSdk | boolean | false | Dev mode: stderr tracer, no setup[^3] | | exporterUrl | string | — | OTLP/HTTP traces endpoint[^4] | | enabled | boolean | true | false disables all instrumentation | | enableMetrics | boolean | true | false disables mcp.tool.* metrics only | | fingerprinting | boolean | true | false disables mcp.failure.* attributes |

[^2]: Required only when setupNodeSdk is true. Has no effect otherwise — the host app's registered TracerProvider owns the resource; passing it anyway logs a one-time diag.warn. [^3]: Creates and registers a NodeTracerProvider that always prints to stderr (safe alongside stdio-transport servers — ADR 003), additionally exporting via OTLP/HTTP if exporterUrl is set. [^4]: Only takes effect when setupNodeSdk is true.

Ordering constraint

Instrumentation works by wrapping the tool-call handler at the moment it's registered. If a handler is registered before instrumentMcpServer() runs, that handler was never wrapped — it slipped past the trap before it was set.

Call instrumentMcpServer() before registering any tool handlers — before server.setRequestHandler(CallToolRequestSchema, ...) (low-level Server) or before any .tool()/.registerTool() call (McpServer). See ADR 002 in docs/adr/ for the detection logic that catches violations of this at instrument time.

Two modes

Quick dev setup

setupNodeSdk: true sets up a NodeTracerProvider that prints spans to stderr (safe alongside stdio-transport MCP servers — see ADR 003), optionally plus an OTLP exporter if exporterUrl is provided. No separate OTel SDK setup needed — serviceName is required in this mode, since it names the resource of the provider opentel-mcp creates.

Production setup

Omit setupNodeSdk (default false). opentel-mcp uses whatever TracerProvider is already registered via trace.setGlobalTracerProvider(), so it plugs into any existing OTel setup without conflict. The host's TracerProvider owns the resource here, so serviceName is not needed and has no effect — set service.name on the host's Resource instead. Passing serviceName anyway is harmless but logs a one-time diag.warn.

Semantic conventions

0.x — the MCP semantic conventions this library implements are Development-stage, not Stable, and may still change upstream; breaking attribute renames will land in minor versions until 1.0, tracked in release notes rather than silently shipped.

opentel-mcp follows those conventions (published by the OTel GenAI SIG, moved there from the main semantic-conventions repo, where the MCP conventions are now deprecated) for everything they define, and adds two namespaces of its own where they don't yet: mcp.tool.* (call-count and duration metrics) and mcp.failure.* (failure fingerprinting). Both are documented as non-spec at every attribute (src/attributes.js, src/fingerprint/attributes.js), and are candidates to fold into the spec's own metrics/error vocabulary if it grows an equivalent. Full reasoning: ADR 004 in docs/adr/.

Compatibility

  • Node.js 20+
  • Windows, macOS, Linux (CI matrix tested)
  • Pure JavaScript, zero native dependencies
  • Supports both low-level Server and high-level McpServer APIs
  • @modelcontextprotocol/sdk ^1.0.0
  • @opentelemetry/api ^1.

Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.