opentel-mcp-ui
v0.3.0
Published
Zero-infrastructure local dashboard for opentel-mcp -- the silent failures, cost, and thrash loops standard OTel tracing misses
Readme
opentel-mcp-ui
A zero-infrastructure local dashboard for
opentel-mcp— see which of your MCP server's tool calls failed silently, without standing up a backend.
opentel-mcp can tell you that a tool call returned isError: true inside
an otherwise-successful JSON-RPC response — the exact failure a standard
OpenTelemetry setup renders as a clean, green span. This package is the
one-command way to actually look at that signal, with no Prometheus, no
Grafana, no OTel Collector to configure first.
30-second demo
npx opentel-mcp-ui --demoOpens a dashboard at http://127.0.0.1:4319 seeded with realistic
fixture data: 86 tool calls (68 clean successes, 7 ordinary thrown/protocol
failures a standard OTel setup would also catch, and 11 silent failures it
wouldn't), plus a few resource and prompt calls. Two of the tools are
market-data tools whose successful responses are sometimes stale. Nothing to configure, no
MCP server required. The panels:
- Observation matrix — SUCCESS/FAILURE rows × visible-to-OTel/missed-by-OTel columns. The "missed" + "failure" cell is the whole point: calls a plain OTel setup would show as green.
- Tool health — an A–F grade per tool from silent-failure rate, error
rate, thrash episodes, p95 latency and, for tools that report freshness
(opentel-mcp 0.17.0+), stale-data rate. The grade is the worst of them
(latency alone never goes below C), and hovering a grade tells you which
signal set it. Tools with fewer than 10 calls show "Not enough data". With
freshness data, the panel also counts stale responses.
Formula and thresholds:
docs/health-grades.md. - Silent-failure feed — defaults to that exact cell, newest-first,
with the side-by-side comparison of what standard tracing would have
shown versus what actually happened. With opentel-mcp 0.16.0+, a filter
narrows it to errors your agent can't act on:
isError: truewith no content, or under 10 characters of text and nothing else. With opentel-mcp 0.17.0+, rows show the data's age when the tool reported it, and a Stale data filter lists successful responses whose data was older than the tool's own limit ("data 4m old · limit 5s"). See opentel-mcp's "Staleness detection" section for how a tool reports it. - Resources & prompts — with opentel-mcp 0.16.0+'s opt-in
coverage: { resources: true, prompts: true }, calls toresources/*andprompts/*by method (and prompt name), with their failures. Hidden for tools-only servers.
Only tools/call spans count toward the matrix, the hero number, the feed
and the grades. A tools/list or resources/read span isn't a tool call.
- Detector status banner — which of opentel-mcp's four in-memory
trackers (thrash detection, cost/budget, schema drift, tool-outcome
counting) are confidently live for the server you've pointed this at —
stated honestly, not implied by a confident-looking zero. (In
--demomode, since there's no real server, it says so plainly instead of guessing.)
Add --open to launch your browser automatically. Run npx opentel-mcp-ui --help
for the full flag list.
Pointing a real MCP server at it
Drop --demo and start the dashboard against nothing. Until the first
span arrives it shows a Connect your server screen: this instance's
OTLP endpoint and the exact snippet below, already filled in with it.
Then it switches to the live dashboard by itself.
npx opentel-mcp-uiopentel-mcp-ui: dashboard listening at http://localhost:4319
opentel-mcp-ui: OTLP/HTTP JSON trace receiver at http://localhost:4319/v1/traces
opentel-mcp-ui: point your instrumented server's exporterUrl at the URL above (instrumentMcpServer(server, { serviceName: 'my-mcp-server', setupNodeSdk: true, exporterUrl: 'http://localhost:4319/v1/traces' })).Then, in the MCP server you're instrumenting with opentel-mcp core,
point its exporter at that URL — no new dependency, no new mechanism;
this is the same OTLP/HTTP JSON export path setupNodeSdk: true already
ships:
import { instrumentMcpServer } from 'opentel-mcp';
instrumentMcpServer(server, {
serviceName: 'my-mcp-server',
setupNodeSdk: true,
exporterUrl: 'http://127.0.0.1:4319/v1/traces',
});Make real tool calls against your server and the dashboard replaces the connect screen live, over Server-Sent Events, with no page reload needed.
Environment variable, as an alternative to hardcoding the URL:
opentel-mcp core doesn't read an env var for exporterUrl itself (it's
a plain constructor option), so set one yourself and reference it:
OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4319 node your-server.jsinstrumentMcpServer(server, {
serviceName: 'my-mcp-server',
setupNodeSdk: true,
exporterUrl: `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? 'http://127.0.0.1:4319'}/v1/traces`,
});In-process mode: withUI()
If your server runs in the same process you want the dashboard in (dev tooling, a single-process demo), skip the separate CLI process:
import { instrumentMcpServer } from 'opentel-mcp';
import { withUI } from 'opentel-mcp-ui';
const instrumented = instrumentMcpServer(server, { setupNodeSdk: true });
const { url } = await withUI(instrumented, { open: true });
// Point setupNodeSdk's own OTLP exporter back at this URL -- see the
// note below for why that extra step is still required.withUI()'s most reliable ingestion path is the exact same OTLP/HTTP
receiver the standalone CLI exposes (<url>/v1/traces) — it also
attempts to attach directly to your TracerProvider as a bonus, best
effort only, since the installed @opentelemetry/sdk-trace-node (2.x) has
no public API to add a SpanProcessor after construction. Don't rely on
the direct-attach path; wire exporterUrl to withUI()'s own url the
same way the two-process setup above does.
CLI reference
opentel-mcp-ui [options]
--demo Seed a realistic fixture with no live MCP server.
--port=<n> Port to listen on (default: 4319). Binds to
127.0.0.1 only -- never reachable off this machine.
--open Open the dashboard in your default browser.
--stateless Assert the transport is stateless HTTP.
--stateful Assert the transport is a long-lived instance.
--help, -h Show this help and exit.--stateless/--stateful matter only when you have real traffic and
want accurate detector-status reporting: whether opentel-mcp core's
in-memory trackers (thrash detection, cost/budget, schema drift,
tool-outcome counting) can meaningfully accumulate state depends on
whether your server is re-instrumented fresh per request (common under
stateless HTTP deployments) or long-lived (stdio, or a persistent HTTP
process). Auto-detection (the default) makes a best-effort structural
guess from the transport shape and says so honestly when it can't be
sure — pass one of these two flags if you know your own deployment
topology and want a confident answer instead.
What this never does
- Never phones home. No telemetry, no update check, no outbound request beyond what your own instrumented server sends it.
- Never persists anything outside the running process. Spans live in an in-memory, bounded ring buffer; nothing touches disk. Stop the process and the data is gone.
- Never binds to anything but
127.0.0.1. This dashboard has no authentication, so it is never reachable from another machine, even on your own local network, regardless of flags. - Never answers other websites (0.3.0+). Requests must be addressed to
localhost,127.0.0.1or[::1]on the dashboard's own port, and browser requests to/api/*or/v1/tracesmust come from the dashboard itself; anything else gets403. That stops a page open in your browser from reading your spans or posting fake ones, including via DNS rebinding. Reaching it through a port forward? Forward to the same port number it listens on. - Never accepts anything but OTLP/HTTP JSON on
/v1/traces(415otherwise), up to 8 MiB per request (413above that). - Never runs anything the page didn't ship with. The dashboard is served with a strict Content-Security-Policy that allows only its own inline script and stylesheet, by hash.
Version compatibility
opentel-mcp-ui has zero compile-time coupling to opentel-mcp core's
exports — it reads well-known span attribute keys as plain strings
(error.type, mcp.tool.argument_count, …), not imported constants, so
an attribute this UI version doesn't recognize from a newer core simply
doesn't render anything new; it never breaks a build or crashes the
dashboard. Requires opentel-mcp >=0.8.0 <1.0.0 (the version range where
getObservationState() — used for the detector banner when a real
in-process server is attached — has existed). Full reasoning:
docs/adr/022-publish-ui.md
in the main repo.
License
MIT © Thirumalaiboobathi B
