API Reference
Exported functions, types, and options in opentel-mcp v0.10.0: instrumentMcpServer, computeFingerprint, calculateCost, and their real signatures.
opentel-mcp's package root exports one function for instrumentation
(instrumentMcpServer), a small set of functions and types for
Deep Failure Fingerprinting
(computeFingerprint, toSpanAttributes, ATTRIBUTE_KEYS,
METRIC_SAFE_ATTRIBUTES, DEFAULT_CLASSIFIERS), and — new in v0.5.0 —
a set of functions and types for
Cost & Token Attribution (DEFAULT_PRICING,
defaultExtractor, calculateCost). Every signature below is copied
from the package's own src/index.d.ts and the modules it re-exports
from, not reconstructed from behavior.
TL;DR
instrumentMcpServer(server, options?) is the only function most
integrations need — it wraps a Server or McpServer instance and
returns it unchanged (plus an optional shutdown()). The fingerprinting
exports (computeFingerprint, toSpanAttributes, and friends) exist for
advanced cases: calling the fingerprinting pipeline directly, outside of
instrumentMcpServer's automatic per-call wrapping.
What does instrumentMcpServer() accept?
function instrumentMcpServer<T extends Server | McpServer | DuckTypedMcpServer>(
server: T,
options?: InstrumentOptions,
): T & { shutdown?: () => Promise<void> };server is either a low-level Server or a high-level McpServer, from
either of two supported SDKs: @modelcontextprotocol/sdk (v1 — Server
from .../server/index.js, McpServer from .../server/mcp.js) or
@modelcontextprotocol/server (v2, protocol revision 2026-07-28,
v0.10.0+ — same class names, same import-path shape). Both are OPTIONAL
peer dependencies of opentel-mcp — install whichever one(s) you actually
use; see "Does opentel-mcp support MCP v2?" below for what differs
between them. It must be called before any tools/call handler is
registered — before server.setRequestHandler(CallToolRequestSchema, ...) (v1 low-level) or server.setRequestHandler('tools/call', ...)
(v2 low-level — v2 dispatches by a plain method-name string rather than
schema-object identity), or before any .tool()/.registerTool() call
on an McpServer from either SDK. Calling it more than once on the same
server (or on an McpServer and its inner Server interchangeably) is a
no-op after the first call.
As of v0.10.0, an McpServer-shaped object isn't just duck-typed — its
inner server is verified too. instrumentMcpServer() recognizes a
high-level McpServer by two checks together: an outer shape match
(.server.setRequestHandler is a function, and either .tool or
.registerTool is a function) and, new in v0.10.0, that .server
actually is a recognized Server instance from a supported SDK — not
merely an object that happens to expose a setRequestHandler method,
which is all earlier versions checked.
Unrecognized McpServer-shaped objects now throw, not silently no-op
Scope: this only covers an object that passes the outer McpServer
shape check above but whose .server doesn't resolve to a recognized
Server class. An input that doesn't look like a Server or
McpServer at all fails a different, earlier check and throws a
separate error — this callout is about the narrower, shape-matched case
specifically.
Before v0.10.0, an object satisfying just the outer shape check passed
detection and appeared to instrument successfully —
instrumentMcpServer() returned without error,
getThrashSummary/getObservationState were both attached — while
producing zero spans, zero metrics, and zero fingerprinting for every
tool call, silently. As of v0.10.0, that object throws instead — a
plain Error, not a named or exported error class, so it can't be
caught by type or a .code property. Its .message begins:
"opentel-mcp: instrumentMcpServer() detected an object shaped like a high-level McpServer (it has .tool()/.registerTool() and a .server.setRequestHandler() method), but its `.server` property is not a recognized Server instance from either supported MCP SDK"
No escape hatch was added — an override letting a caller bypass the check would defeat the reason this exists.
The message names three possible causes — check them in this order:
- A duplicate or mismatched install of whichever SDK
(
@modelcontextprotocol/sdkor@modelcontextprotocol/server) your server actually came from — trynpm dedupe, or check for multiple installed versions. - That SDK is installed, but isn't resolvable from opentel-mcp's
own location — a monorepo or hoisting issue. Fix is confirming
normal
node_modulesresolution from wherever opentel-mcp itself is installed, notnpm dedupe— a different problem than (1). - An MCP SDK this package doesn't support at all.
A real Server/McpServer from a single, consistently-resolved copy of
either supported SDK is unaffected by this change.
Scope: as of v0.8.0, the tools/call handler (registered via
CallToolRequestSchema) is always wrapped, and the tools/list handler
(registered via ListToolsRequestSchema) is also wrapped whenever
schemaDrift.enabled is true (the default). Requests for resources/*
and prompts/* pass through unwrapped and don't emit spans or metrics.
tools/list now falls under the instrument-first requirement too
Because schemaDrift.enabled defaults to true, instrumentMcpServer()
must now be called before tools/list is registered, not just before
tools/call — for low-level Server users specifically who register
setRequestHandler(ListToolsRequestSchema, ...) themselves.
Registering it first now throws INSTRUMENT_FIRST_ERROR, where it
didn't before v0.8.0. McpServer users are unaffected — it registers
tools/list and tools/call together, atomically. Migration: reorder
the tools/list registration to after instrumentMcpServer(), or pass
schemaDrift: { enabled: false }.
It returns the same object it was given, so it can be used inline:
const server = instrumentMcpServer(new McpServer({ name: 'my-server', version: '1.0.0' }), {
serviceName: 'my-mcp-server',
});InstrumentOptions
interface InstrumentOptions {
serviceName?: string;
exporterUrl?: string;
enabled?: boolean; // default: true
enableMetrics?: boolean; // default: true
setupNodeSdk?: boolean; // default: false
costTracking?: CostTrackingOptions; // see below
fingerprinting?: boolean; // default: true — deep-failure fingerprinting
thrashDetection?: Partial<ThrashConfig>; // default: enabled — see README's "Agent Thrash Detection (v0.6.0+)" section for ThrashConfig's full fields
schemaDrift?: Partial<SchemaDriftConfig>; // default: enabled — see README's "Tool schema drift detection (v0.8.0+)" section for SchemaDriftConfig's full fields
instanceKey?: string; // default: unset — shares Agent Thrash Detection, budget tracking, schema drift, and the toolOutcome counter's state across instrumentMcpServer() calls that share this key (v0.9.0+). See "Tracker State Under Stateless HTTP" (/docs/instance-state)
}| Option | Default | What it does |
|---|---|---|
serviceName | — | Names the resource of the tracer provider opentel-mcp creates. Required (non-empty string) when setupNodeSdk: true — throws otherwise. Has no effect when setupNodeSdk is false; passing it anyway logs a one-time diag.warn rather than erroring. |
exporterUrl | — | OTLP/HTTP traces endpoint (e.g. http://localhost:4318/v1/traces). Only takes effect when setupNodeSdk: true. |
enabled | true | false makes instrumentMcpServer() a complete no-op. |
enableMetrics | true | false disables the mcp.tool.* metrics; tracing is unaffected. Metrics are already a zero-overhead no-op when no MeterProvider is registered — this is for opting out even when one is. |
setupNodeSdk | false | true makes opentel-mcp create and register its own NodeTracerProvider (always to stderr; also to exporterUrl via OTLP/HTTP if set). false (the default) emits spans through whatever TracerProvider the host application already registered globally, or drops them silently if none has been — so opentel-mcp never overrides a host app's own OpenTelemetry setup. |
costTracking | { enabled: true, pricingTable: DEFAULT_PRICING, extractor: defaultExtractor } | Controls Cost & Token Attribution — see CostTrackingOptions below. Any fields omitted from a partial object fall back to their defaults individually. |
fingerprinting's type-declaration gap — since resolved
src/config.js's resolveOptions() reads a fingerprinting option
(boolean, default true — see Deep Failure
Fingerprinting) and src/instrument.js
uses it at runtime. Through v0.5.0, it wasn't listed in the
InstrumentOptions TypeScript interface in src/index.d.ts — only in
config.js's JSDoc typedef — so TypeScript consumers could see a type
error passing { fingerprinting: false } even though it was honored at
runtime. Resolved in v0.6.0: fingerprinting?: boolean landed on
InstrumentOptions. A narrower, related gap is still not resolved as
of v0.10.0: computeFingerprint()'s classifiers/stackFrames
options remain unwired through instrumentMcpServer()'s own options —
no target version has been set for that part.
What do I get back from instrumentMcpServer()?
The same object passed in — T, unchanged — plus an optional
shutdown(): Promise<void>. shutdown is only attached when
setupNodeSdk: true; check for its presence before calling it. It
flushes and shuts down the NodeTracerProvider opentel-mcp created, and
should run during your process's own shutdown sequence so buffered spans
aren't lost.
DuckTypedServer
type DuckTypedServer = {
setRequestHandler: (...args: any[]) => any;
};The low-level counterpart to DuckTypedMcpServer below — a purely
structural type describing a low-level Server's shape without
importing either SDK's actual Server class. Added in v0.10.0 alongside
@modelcontextprotocol/server becoming a second, optional peer
dependency: once both SDKs are optional, a nominal import type { Server } from '@modelcontextprotocol/sdk/...' in index.d.ts breaks
type-checking for any consumer who has neither SDK installed — confirmed
against a real packed tarball with neither SDK present, not predicted
from package.json syntax alone. Same reasoning as DuckTypedMcpServer's
own history below, which hit the identical problem first.
DuckTypedMcpServer
type DuckTypedMcpServer = {
server: { setRequestHandler: (...args: any[]) => any };
tool?: (...args: any[]) => any;
registerTool?: (...args: any[]) => any;
};A structural type for McpServer-shaped objects that aren't literally
instanceof McpServer — the package's own doc comment explains why:
McpServer has private fields, so TypeScript treats assignability to it
as effectively nominal, and neither SDK's real McpServer class is
imported here at all — nominally importing either would break
type-checking for a consumer who has only the other SDK, or neither,
installed.
Satisfying this type is necessary, but not sufficient, for runtime acceptance
Before v0.10.0, an object satisfying this shape reliably worked at
runtime too — detection there was equally loose. That stopped being
true once detectServerKind() was hardened to additionally require
.server instanceof <Server> for a real, resolved SDK class (see
"Unrecognized McpServer-shaped objects now throw, not silently no-op"
above). As a direct, deliberate consequence, an object that merely has
the right shape — a dual-package-hazard .server from a different
resolved copy of the SDK than this process itself resolves, or a
hand-rolled mock not actually built on either SDK — is now rejected
at runtime (UNWRAPPABLE_MCPSERVER_ERROR), not silently accepted. This
type mirrors the duck-typing the runtime's detection step performs; it
can't also encode the nominal instanceof check runtime acceptance
ultimately requires, since that would mean importing a class this file
cannot safely import for either SDK.
Does opentel-mcp support MCP v2?
Yes, as of v0.10.0. Both @modelcontextprotocol/sdk (v1, protocol
revisions through 2025-11-25) and @modelcontextprotocol/server (v2,
protocol revision 2026-07-28) are supported — two separate, OPTIONAL peer
dependencies. Install whichever one(s) you actually use; neither is
required just to depend on opentel-mcp. Detection and wrapping happen
automatically, resolved once per instrumentMcpServer() call by which SDK
the object actually came from — there's no separate instrumentMcpServerV2()
export.
What works identically to v1: spans, standard attributes, Deep
Failure Fingerprinting, and
mcp.failure.channel/mcp.failure.validation_paths classification — v2
gained its own code path for each, since v2's thrown errors carry a clean
message with no "MCP error N: " wrapper to unwrap, and its rendered
validation-issue text uses a third, distinct format from either of v1's
two. One attribute source differs: jsonrpc.request.id reads from v1's
extra.requestId, but from v2's ctx.mcpReq.id — a field that's always
present on v2 rather than conditionally set, unlike v1's.
v2's own entry points construct a fresh server per request — instrumentMcpServer() must run inside that factory
v2's createMcpHandler/serveStdio build a new Server/McpServer
instance per request, via a factory function you provide — including
createMcpHandler's default stateless posture, not an edge case.
instrumentMcpServer() has to run inside that factory, on every
invocation, not once at module load the way a long-lived v1 server does.
This isn't a new mechanism: it's the identical deployment shape
Tracker State Under Stateless HTTP already
covers for v1's "stateless Streamable HTTP" case — instanceKey (v0.9.0)
is the existing fix, shared unchanged. Nothing v2-specific was added for
it.
Status of the two tracked gaps under v2 (docs/known-gaps.md entries 6
and 8) as of v0.10.0: entry 8 is fixed; entry 6 is partially fixed, not
closed — its fallback-id half is fixed, but a structural limitation
survives it (below). isSingleConnectionTransport() no longer auto-detects the transport
createMcpHandler builds internally as single-connection (entry 8,
fixed) — so Agent Thrash Detection's session-id fallback path is no
longer reached automatically by a typical v2 HTTP deployment; it now
requires an explicit, informed assumeSingleSession: true opt-in, or a
genuinely single-connection v2 serveStdio deployment. For whichever
deployments do legitimately reach that fallback path, the generated
fallback session id is now shared across repeated instrumentMcpServer()
calls when instanceKey is set (entry 6's fallback-id half, fixed) — the
same registry Tracker State Under Stateless HTTP
already describes, one more namespaced entry, no new bound/eviction
policy. One narrower thing stays open: the internal flag tracking
"has this server ever proven itself session-aware" isn't registry-backed
yet, which only matters for a deployment mixing real-session-id calls
with occasional no-session-id ones under one shared instanceKey.
Separately, and not fixable by this or any library-side change: MCP spec
2026-07-28 removes the wire-level session handshake entirely, so
createMcpHandler's stateless default simply doesn't populate a real
session id most of the time — instanceKey was never designed to
manufacture one, only to share tracker state given one.
What's exported for Deep Failure Fingerprinting?
These are re-exported at the package root from src/fingerprint/, so
they're available as import { computeFingerprint, ... } from 'opentel-mcp' rather than reaching into a subpath.
function computeFingerprint(
err: unknown,
ctx: FingerprintContext,
opts?: ComputeFingerprintOptions,
): FingerprintResult;interface FingerprintContext {
readonly toolName?: string;
readonly origin: FailureOrigin;
readonly cwd?: string; // default: process.cwd(), used for path normalization
}
interface ComputeFingerprintOptions {
readonly classifiers?: readonly Classifier[]; // prepended to DEFAULT_CLASSIFIERS
readonly stackFrames?: number; // default: 5
}computeFingerprint() never throws — see
Deep Failure Fingerprinting for the fallback
behavior and why that matters for something running inline in the
instrumentation path.
function toSpanAttributes(result: FingerprintResult): Attributes;Maps a FingerprintResult onto the five mcp.failure.* span attributes.
Returns {} if result is malformed in any way — like
computeFingerprint(), this must never throw, since it also runs inline
on every failure.
const ATTRIBUTE_KEYS: {
FINGERPRINT: 'mcp.failure.fingerprint';
SIGNATURE: 'mcp.failure.signature';
CATEGORY: 'mcp.failure.category';
ORIGIN: 'mcp.failure.origin';
ERROR_CLASS: 'mcp.failure.error_class';
};
const METRIC_SAFE_ATTRIBUTES: readonly ['mcp.failure.category', 'mcp.failure.origin'];
const DEFAULT_CLASSIFIERS: readonly Classifier[]; // 7 built-in classifiers, in match orderWhat types does opentel-mcp export?
type FailureCategory =
| 'validation' | 'timeout' | 'network' | 'auth'
| 'dependency' | 'serialization' | 'internal' | 'unknown';
type FailureOrigin = 'tool_error' | 'thrown' | 'transport';
interface FingerprintResult {
readonly fingerprint: string; // 16 lowercase hex chars
readonly signature: string; // "<errorClass>@<fn>:<line>", ≤60 chars
readonly category: FailureCategory;
readonly origin: FailureOrigin;
readonly inputs: FingerprintInputs;
}
interface FingerprintInputs {
readonly errorClass: string;
readonly category: FailureCategory;
readonly origin: FailureOrigin;
readonly toolName: string | null; // set only for origin: 'tool_error'
readonly normalizedMessage: string;
readonly stackSignature: string;
}What's exported for Cost & Token Attribution?
New in v0.5.0, re-exported at the package root from src/cost/ the same
way the fingerprinting exports are — import { DEFAULT_PRICING, defaultExtractor, calculateCost } from 'opentel-mcp'. See
Cost & Token Attribution for how these fit
together at runtime.
const DEFAULT_PRICING: PricingTable; // 15+ models across 5 providers, see src/cost/pricing.js
const defaultExtractor: UsageExtractor;
function calculateCost(
inputTokens: number,
outputTokens: number,
model: string,
pricingTable: PricingTable,
): number | null; // null if model isn't in pricingTable or a token count is invalidinterface CostTrackingOptions {
enabled?: boolean; // default: true
pricingTable?: PricingTable; // default: DEFAULT_PRICING
extractor?: UsageExtractor; // default: defaultExtractor
budget?: BudgetConfig; // default: undefined (budget tracking off)
}
interface ModelPricing {
readonly inputPer1M: number; // USD per 1,000,000 input tokens
readonly outputPer1M: number; // USD per 1,000,000 output tokens
readonly currency: 'USD';
}
type PricingTable = Record<string, ModelPricing>;
interface TokenUsage {
readonly inputTokens: number;
readonly outputTokens: number;
readonly totalTokens: number;
readonly model?: string; // present only when the extractor found a model name
}
type UsageExtractor = (toolResult: unknown) => TokenUsage | null;
interface BudgetConfig {
readonly perSessionUsd?: number; // cumulative-cost limit per MCP session id
readonly perToolUsd?: number; // cumulative-cost limit per tool name
}calculateCost() normalizes model (lowercases it, strips a leading
provider/ prefix) before looking it up in pricingTable, and never
throws — an unrecognized model or an invalid token count resolves to
null, not an exception. defaultExtractor also never throws; see
Cost & Token Attribution for the shapes it
recognizes.
Where do I go from here?
- Deep Failure Fingerprinting — what these fingerprinting exports are for and how the hashing pipeline works.
- Cost & Token Attribution — how
CostTrackingOptionsand the exports above fit together at runtime. - Metrics — the six
mcp.tool.*instrumentsinstrumentMcpServer()records. - Silent Failures — the
isErrordetection this whole package exists for.