Metrics
The four mcp.tool.* OpenTelemetry metrics opentel-mcp emits — calls, errors, silent_failures, and duration — with SigNoz and Prometheus query examples.
opentel-mcp emits four OpenTelemetry metrics instruments through
@opentelemetry/api's Metrics API: mcp.tool.calls, mcp.tool.errors,
mcp.tool.silent_failures, and mcp.tool.duration. All four come from
src/metrics.js's setupMeter(), recorded from the same
isToolResultError() check that marks spans
ERROR, so the metric and the trace never disagree about whether a call
failed.
TL;DR
Four instruments, all under the mcp.tool.* namespace:
mcp.tool.calls (every call), mcp.tool.errors (thrown/rejected),
mcp.tool.silent_failures (isError: true), and mcp.tool.duration
(histogram, milliseconds). Nothing is recorded until your application
registers an OpenTelemetry MeterProvider — until then, recording
through these instruments is a zero-overhead no-op, the default
@opentelemetry/api behavior.
What are the four instruments?
| Metric | Type | Unit | Attributes | Emitted when |
|---|---|---|---|---|
mcp.tool.calls | Counter | — | gen_ai.tool.name, mcp.method.name | Every tool call, regardless of outcome |
mcp.tool.errors | Counter | — | gen_ai.tool.name, error.type1 | Handler threw or its promise rejected |
mcp.tool.silent_failures | Counter | — | gen_ai.tool.name1 | Result had isError: true |
mcp.tool.duration | Histogram | ms | gen_ai.tool.name, mcp.tool.outcome1 | Every call, on completion |
mcp.tool.duration's mcp.tool.outcome attribute is one of success,
error, or silent_failure — the same three-way split as the
instrument names, but on a single histogram so you can compare call
latency across all three outcomes without joining against error.type
(which silent failures don't set on the duration metric).
Why is mcp.tool.silent_failures a separate counter from mcp.tool.errors?
Because they're different failure modes and mixing them into one counter
would hide which one is happening. mcp.tool.errors counts handlers that
threw or rejected — a JavaScript-level exception. mcp.tool.silent_failures
counts handlers that returned normally with CallToolResult.isError: true — see Silent Failures for why that case is
easy to miss entirely without this counter. A tool that's failing by
throwing and a tool that's failing by returning isError: true usually
point at different bugs; a single alert on both would tell you that
something's wrong but not what.
Do I need to configure anything to get these?
No setup beyond registering a MeterProvider somewhere in your
application — instrumentMcpServer() doesn't create one for you (unlike
its tracing side, which can optionally create its own NodeTracerProvider
via setupNodeSdk: true). Set enableMetrics: false in
instrumentMcpServer()'s options to opt out of
metrics specifically while keeping tracing; tracing has no equivalent
metrics-only switch since it isn't one.
How do I query these in SigNoz?
Illustrative
The wiring code below is real — copied from opentel-mcp's own README. The query text is illustrative: it shows the shape of a useful query against these metric names, not a query captured from a live SigNoz instance.
Wiring a MeterProvider that exports to SigNoz's default local OTLP/HTTP
endpoint:
import { metrics } from '@opentelemetry/api';
import { MeterProvider, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
import { resourceFromAttributes } from '@opentelemetry/resources';
const meterProvider = new MeterProvider({
resource: resourceFromAttributes({ 'service.name': 'my-mcp-server' }),
readers: [
new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({ url: 'http://localhost:4318/v1/metrics' }),
}),
],
});
metrics.setGlobalMeterProvider(meterProvider);With that registered before instrumentMcpServer() runs, a SigNoz
dashboard panel querying mcp.tool.silent_failures grouped by
gen_ai.tool.name shows which tools are failing silently, and how often,
without opening a single trace.
How do I export these to Prometheus?
Register a Prometheus exporter as the MeterProvider's reader instead of
an OTLP one — nothing about instrumentMcpServer() changes. Verified
against @opentelemetry/exporter-prometheus current stable — pin your
version and check the package README if you upgrade:
import { metrics } from '@opentelemetry/api';
import { MeterProvider } from '@opentelemetry/sdk-metrics';
import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
const meterProvider = new MeterProvider({
readers: [new PrometheusExporter({ port: 9464 })],
});
metrics.setGlobalMeterProvider(meterProvider);OpenTelemetry's Prometheus exporter converts metric names by replacing
. with _ and appending _total to counters, so
mcp.tool.silent_failures becomes mcp_tool_silent_failures_total in
PromQL:
sum(rate(mcp_tool_silent_failures_total[5m])) by (gen_ai_tool_name)That gives a per-tool silent-failure rate over a 5-minute window — a reasonable starting alert query, tune the window and threshold to your own traffic.
Where do I go from here?
- Silent Failures — what
isError: truemeans and whymcp.tool.silent_failuresexists at all. - Deep Failure Fingerprinting — the
mcp.failure.categoryattribute these metrics carry when fingerprinting classifies a failure. - API Reference —
enableMetricsand the rest ofinstrumentMcpServer()'s options.
Footnotes
-
Also carries
mcp.failure.categorywhen Deep Failure Fingerprinting classified the failure — see that page for the 8 categories. ↩ ↩2 ↩3