OtlpDashboard
Provides read-only MCP tools for querying OpenTelemetry telemetry in the dashboard, including logs, traces, metrics, service lists, individual trace details, and metric points.
OpenTelemetry Dashboard
"Grafana was overwhelming, while Aspire was lacking."
This dashboard is the "just right" middle ground: a lightweight, self-hosted OTLP receiver and viewer designed for those who need professional observability without the setup fatigue.

A self-hosted OTLP receiver and viewer for traces, logs, and metrics.
.NET 10 + EF Core backend, Vue 3 / Nuxt 4 SPA. Stores telemetry in SQLite,
PostgreSQL, or SQL Server. Speaks OTLP gRPC on :4317 and OTLP
HTTP/Protobuf on :4318 (the same port also serves the SPA).
Website: adiakys.github.io/OtlpDashboard
Live demo: adiakys.github.io/OtlpDashboard/demo — static build with mock telemetry, no backend required. Login accepts any password (e.g. demo).
The demo runs against an in-browser mock backend (a tiny TS shim that intercepts every API call and returns generated DTOs) — not the real .NET host. It exists to let people click around without us hosting anything; behaviour can drift from the real server in subtle ways (some flows are stubbed, some validation paths return generated data instead of real errors). For a faithful preview run the docker compose stack below.
Quick start
Docker — full demo stack
The bundled docker-compose.yml starts the dashboard against Postgres plus a
small demo workload (sample-server + sample-client + the OpenTelemetry
Collector) that pushes traces, logs, and metrics so the dashboard isn't empty
on first boot:
docker compose up --build
# OTLP gRPC : localhost:4317
# OTLP HTTP : localhost:4318
# SPA : http://localhost:4318Sources for the demo workload live under demo/ — sample app
(Server / Client), the built-in widget-library pack
(demo/packs/default/, distributed as a separate repo and consumed via
git submodule), the demo-stack starter dashboard
(demo/packs/default-dashboard/), and the Collector config. To run only the dashboard against an external Postgres / SQL Server,
strip the demo services from your override file and point
Dashboard__Storage__Provider + ConnectionStrings__* at your DB.
Local dev (no Docker)
# Backend
dotnet run --project src/OpenTelemetryDashboard.Host
# Frontend (separate terminal, hot-reload, proxies /api to :4318)
cd web && pnpm install && pnpm devThe backend runs migrations on every boot, so the SQLite file appears at
./src/OpenTelemetryDashboard.Host/data/telemetry.db on first run.
Related MCP server: otel-mcp-server
Configuration
All settings live under Dashboard:* in appsettings.json and can be
overridden by environment variables using double underscores
(e.g. Dashboard__BrowserToken).
Auth tokens
Two static bearer tokens gate the surface:
Variable | Protects |
| The SPA + read API ( |
| OTLP ingestion (gRPC + HTTP) |
Auth is opt-in per surface, identically in Development and Production:
Set a token → that surface requires it.
Leave a token empty → that surface is public (allow-all). Leaving
DASHBOARD__BROWSERTOKENempty also makes the SPA skip the login screen (/api/v1/inforeportsrequireAuth: false), so the dashboard opens straight away — convenient for a local Docker / Docker Compose run.
Either unset token surfaces as Degraded on /healthz (in any
environment) and is logged at startup, so an open deployment is visible
rather than silent.
The SPA's /login form exchanges the password for an HttpOnly +
SameSite=Strict session cookie via POST /api/v1/auth/login; JS never
sees the token after the exchange. POST /api/v1/auth/logout clears
the cookie. OTLP clients send the API key as
Authorization: Bearer <token> or as the x-otlp-api-key header.
Retention (max days)
Background job that drops rows older than the configured age. Defaults target a "debug + recent forensics" workload — logs hold a month so last-week incidents stay investigable; traces and metrics rotate weekly because high-cardinality spans and split-by metric attributes inflate fast.
"Dashboard": {
"TelemetryLimits": {
"MaxLogDays": 30, // default
"MaxTraceDays": 7, // default
"MaxMetricDays": 7, // default
"SweepIntervalMinutes": 60
}
}Or via env vars:
Dashboard__TelemetryLimits__MaxLogDays=30
Dashboard__TelemetryLimits__MaxTraceDays=7
Dashboard__TelemetryLimits__MaxMetricDays=7
Dashboard__TelemetryLimits__SweepIntervalMinutes=60A value of 0 disables retention for that signal (records are kept
indefinitely) — /healthz then reports Degraded so an unbounded
window can't hide silently. The sweep runs each SweepIntervalMinutes
per signal independently — a failure on one kind doesn't stop the
others.
Storage provider
Dashboard__Storage__Provider=Sqlite # default
ConnectionStrings__Sqlite="Data Source=/app/data/telemetry.db"
Dashboard__Storage__Provider=PostgreSql
ConnectionStrings__PostgreSql="Host=...;Database=telemetry;Username=...;Password=..."
Dashboard__Storage__Provider=SqlServer
ConnectionStrings__SqlServer="Server=...;Database=telemetry;User Id=...;Password=...;TrustServerCertificate=True"MCP server (read-only)
An optional Model Context Protocol server exposes the same data as the read API (logs, traces, metrics) so an LLM client — Claude Code, Claude Desktop, MCP Inspector, etc. — can query the dashboard directly. Disabled by default; set the flag to mount it:
Dashboard__Mcp__Enabled=trueMounted at /mcp on the same port as the SPA (:4318) over Streamable HTTP,
gated by the same browser bearer token (DASHBOARD__BROWSERTOKEN) that
protects /api/v1/*. Tools exposed:
Tool | Mirrors |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Wire it into Claude Code by dropping a .mcp.json at your project root:
{
"mcpServers": {
"otel-dashboard": {
"type": "http",
"url": "http://localhost:4318/mcp",
"headers": { "Authorization": "Bearer ${DASHBOARD_BROWSER_TOKEN}" }
}
}
}Built on the official ModelContextProtocol.AspNetCore SDK
(jointly maintained by Anthropic and Microsoft).
Widgets
Dashboards are made of widgets dropped onto a 12-column grid. Three sources:
std— built into the bundle (Stat, Line, Sparkline, Gauge, Bar gauge, Pie, Heatmap, Recent traces, Top traces, Logs stream, Text).custom— preset of a builtin saved by the user, persisted in the DB.<library>— read-only widgets shipped by an installed library (filesystem or installed from a Git repo).
The picker dialog (+ Add widget while editing a dashboard) shows all
three groups in the same modal. The search box filters by name,
description, or source — type std, custom, or a library id to filter
a whole bucket.
Custom legend labels
Line, Pie, Bar gauge and Heatmap widgets label their series, slices, bars or
rows with the metric's attributes (Line adds the metric name when it shows
several), which can get long. To shorten them, open the widget's config drawer and fill the
Legend field (one per selected metric on Line). Use {attribute} to
insert an attribute value, e.g. Requests {http.request.method} renders as
Requests POST. Leave it empty to keep the default label. In pack JSON the
same value is the legend field of the metric binding.
Creating a custom widget
Open a dashboard, click Edit, add a builtin widget.
Configure it (metric binding, thresholds, range, …).
In the config drawer click Save as widget, give it a name, icon, and default size.
It now appears in the picker under My widgets, with inline edit and delete buttons that show on hover.
Notes:
The seed config is captured at save time — editing the template later changes only metadata (name, description, icon, default size). The seed config is immutable; clone + delete to change it.
Existing dashboard instances of a deleted custom widget render a placeholder, not a crash.
Packs (widget libraries + dashboards)
Everything that can ship with the dashboard — widget libraries, built-in
dashboards, and any future asset type — is distributed as a pack: a
single directory with pack.json at its root. The dashboard scans every
entry in Dashboard:Packs:Paths (in order) and surfaces every pack's
contents in the picker (libraries) and dashboard list (built-ins).
The shipped image already configures two paths in scan order:
/app/data/packs— runtime-managed (volume, git installs, drag-and-drop)/app/builtin-packs— baked into the image layer (no volume shadowing on rebuild)
Derived images don't need to set any environment variable — just COPY
into the second path:
FROM opentelemetrydashboard:latest
COPY my-pack/ /app/builtin-packs/my-pack/When two paths expose packs with the same pack.json#id, the first in
scan order wins and the rest are skipped with a warning — so a runtime
install can override a baked-in default by sharing its id.
Two sample packs live under demo/packs/:
default/(a submodule pointing atAdiakys/OtlpDashboard-default-pack) ships the .NET and PostgreSQL widget libraries.default-dashboard/ships the starter dashboard pre-bound to the demo compose stack.
Both are bind-mounted by docker-compose.yml, so docker compose up --build
shows libraries + starter dashboard out of the box.
Install one of two ways:
Drop a folder. Copy the pack into the packs path (volume-mount, Ansible, etc.). Click the refresh icon in the widget picker (or
POST /api/v1/packs/reload) to re-scan.Install from a Git repo. Click the git-branch icon in the picker header (or
POST /api/v1/packs/installwith{ url, ref, path? }). The server runs a shallow clone via LibGit2Sharp, parsespack.json, resolves HEAD to a commit SHA, and atomically moves the directory into the runtime-managed root. Allowed hosts areDashboard:Packs:AllowedGitHosts(defaultgithub.com, gitlab.com). Use a tag for stable pinning; branches work but get a UI warning. The optionalpathparameter re-roots the install on a sub-directory, useful for monorepos that ship multiple packs side by side. Updates: the "Update" button on git-installed packs re-pulls the same ref (fetch && reset --hard).
Pack layout
my-pack/
├── pack.json
├── README.md (optional — surfaced in the UI)
├── LICENSE (optional)
├── libraries/
│ ├── core/
│ │ ├── manifest.json
│ │ └── widgets/
│ │ ├── sla-tracker/widget.json
│ │ └── trace-heatmap/widget.json
│ └── extras/
│ ├── manifest.json
│ └── widgets/…
└── dashboards/
├── default.json
└── ops-overview.jsonpack.json:
{
"id": "team-otel-pack",
"name": "Team OTel Pack",
"version": "1.2.0",
"author": "platform@example.com",
"license": "MIT",
"description": "Curated widgets and dashboards for service ownership reviews",
"homepage": "https://github.com/example/team-otel-pack",
"libraries": [
{ "id": "core", "path": "libraries/core" },
{ "id": "extras", "path": "libraries/extras" }
],
"dashboards": [
{ "id": "default", "path": "dashboards/default.json", "builtin": true },
{ "id": "ops-overview", "path": "dashboards/ops-overview.json" }
]
}id must match the directory name. Each library entry's
manifest.json carries only id, name, description?, icon? —
shipping metadata (version, author, license) lives at pack level.
Dashboards flagged builtin: true are seeded into the dashboard store
on first boot; the rest are installable templates.
Writing a widget
Each widgets/<kindId>/widget.json declares one widget. Two engines:
preset — wraps a builtin with a precooked config:
{
"name": "SLA Tracker",
"description": "p99 latency with SLO thresholds",
"icon": "i-ph-target",
"defaultSize": { "w": 4, "h": 3 },
"engine": "preset",
"baseKind": "metric-stat",
"defaultConfig": {
"calc": "last",
"unitKind": "ms",
"decimals": 1,
"thresholds": [
{ "value": 0, "color": "#7AAA7A" },
{ "value": 200, "color": "#D9B566" },
{ "value": 500, "color": "#E27A3F" }
]
}
}baseKind is one of: metric-stat, metric-line, metric-sparkline,
metric-gauge, metric-bar-gauge, metric-pie, metric-heatmap,
recent-traces, top-traces, logs-stream, text. The shape of defaultConfig
matches the corresponding form in the SPA — copy from a working instance
(Save as widget produces a valid one).
spec — sandboxed HTML/SVG/CSS template with named metric bindings.
Use this for card-style UIs that the standard chart widgets can't
express: a database illustration whose fill level tracks a load
metric, a grid of service tiles whose colour follows a per-service
status metric, gauges with custom artwork, etc.
{
"name": "Database card",
"icon": "i-ph-database",
"defaultSize": { "w": 4, "h": 4 },
"engine": "spec",
"spec": {
"template": "<div class='db'>\n <svg>...</svg>\n <div class='stats'>\n <strong class='{{ thresholdClass load.value load.thresholds }}'>{{ format load.value 'percent' 0 }}</strong>\n <span>QPS {{ format qps.value 'ops' 1 }}</span>\n </div>\n</div>",
"styles": ".db { display: flex; gap: 1rem; ... } .db .vellum-th-bad { color: var(--color-rust-600); }",
"dataBindings": [
{ "name": "load", "type": "metric", "calc": "last", "unitKind": "percent",
"thresholds": [{ "value": 0, "color": "#7AAA7A" }, { "value": 70, "color": "#D9B566" }, { "value": 90, "color": "#E27A3F" }] },
{ "name": "qps", "type": "metric", "calc": "mean", "unitKind": "ops" }
]
}
}The user installing the library picks one instrument per binding
(load, qps, …) via the config drawer; the template/styles are
immutable and ship with the library.
Template syntax (Mustache-light, no JS evaluation):
Construct | Meaning |
| Interpolate a value (HTML-escaped). |
| Call a whitelisted helper. |
| Render block when truthy. |
| Loop, exposes |
Helpers available: format(value, unitKind, decimals?),
percent(value, min, max), thresholdColor(value, thresholds),
thresholdClass(value, thresholds) (returns vellum-th-ok / -warn
/ -bad), dateAgo(timestamp), dateTime(timestamp, preset?) (absolute
date/time honoring the OS 12h/24h preference; preset defaults to datetime,
also time/time-seconds/time-ms/datetime-seconds/datetime-long),
pluralize(n, singular, plural), default(...values), comparators
eq/neq/lt/lte/gt/gte (usable inside {{#if}}).
Binding shapes the template scope sees:
metric(default —splitBynot set):{ value, unit, unitKind, thresholds }.metricwithsplitBy: "service.name": array of{ key, value, attrs, thresholds }— iterable with{{#each}}.metric-series: array of raw{ time, value, attributes }rows.
Sandboxing:
The Mustache renderer never
evals — only whitelisted helpers run.DOMPurify (lazy-loaded ~50 KB gzip) sanitises the rendered HTML: no
<script>, noon*=, nojavascript:URLs, no<iframe>.CSS is auto-prefixed with the widget's instance scope so styles can't leak;
@import,@font-face,expression(), and IE-era binding tricks are stripped before scoping.
Field rules enforced by the loader (engine: spec):
name≤ 64 chars.description≤ 280 chars.iconmatches^i-(ph|lucide)-[a-z0-9-]+$.defaultSize.w∈ [1, 12],defaultSize.h∈ [1, 24].defaultConfig≤ 64 KiB;spec(the whole template+styles+bindings envelope) ≤ 256 KiB.
Invalid widgets are skipped (logged) and don't break the rest of the library.
The bundled demo/packs/default/libraries/postgres/ library ships
postgres-server-card and db-scoreboard — two engine: spec widgets
that double as live examples for authors of the spec engine.
Built-in dashboards
Dashboards ship inside packs — list them in pack.json#dashboards with
builtin: true and the BuiltinDashboardSeeder folds each into the
dashboard store on the first boot after install. The format matches the
one the SPA emits via "Export JSON".
File format
Same envelope produced by the SPA's export, plus an optional top-level
id:
{
"version": 1,
"id": "00000000-0000-0000-0000-000000000001",
"name": "Production Overview",
"widgets": [
{ "id": "...", "kind": "std:metric-stat", "x": 0, "y": 0, "w": 4, "h": 3, "config": { /* opaque */ } }
]
}Id resolution (precedence)
Explicit
idin the JSON wins, must parse as a Guid.default.jsonfilename →00000000-0000-0000-0000-000000000001(the well-known default dashboard id). Convenient for distributing a non-empty default.Any other filename → SHA-256 of the filename, truncated to 16 bytes (RFC 9562 v8). Stable across deployments — same filename always produces the same dashboard id.
Idempotency
The seeder runs every boot but uses skip-if-exists semantics: an id
already in the store is left alone. The single special case is the
default dashboard — when default.json is present and the existing
default row is still pristine (no widgets, RowVersion 0), the seeder
upserts it once. Any user edit (saved widgets or RowVersion ≥ 1) makes
the seeder back off.
To re-apply a built-in file, delete the dashboard via the UI first.
A demo lives at demo/packs/default-dashboard/dashboards/default.json
(referenced from default-dashboard/pack.json with builtin: true),
mounted by docker-compose.yml as /app/builtin-packs/default-dashboard.
Endpoints
Method | Path | Purpose |
GET |
| Liveness + readiness probe (anonymous; checks DB, sink, retention, auth posture) |
POST |
| OTLP HTTP/Protobuf ingestion |
POST |
| Session-cookie login / logout (anonymous) |
GET |
| App name (anonymous); version + retention + storage provider when authenticated |
GET |
| Query API (paginated). |
GET/POST/PUT/DELETE |
| Dashboards CRUD. |
GET/POST/PUT/DELETE |
| Custom widgets CRUD. |
GET |
| Flat library list for the picker |
GET |
| Installed packs (libraries + dashboards) |
POST |
| Re-scan the packs paths |
POST |
| Clone from git ( |
POST |
| Re-pull a git-installed pack |
DELETE |
| Uninstall a pack |
POST/GET/DELETE |
| MCP server (when |
(boot) | seed dashboards from packs |
|
Errors return RFC 7807 application/problem+json with a traceId
extension for log correlation. Per-IP rate limits apply to OTLP ingest
(200 req/s default), the read API (60 req/s), mutations (10 req/s),
and pack install (1 concurrent globally) — tunable under
Dashboard:RateLimits:*.
Acknowledgements
Claude Code did a lot of the typing on this project. The direction, design, testing, and debugging stayed on me.
License
Available Tools
8 toolsget_traceARead-onlyIdempotent
Return every span belonging to the given trace id, with each span's resource service.name. Errors if no spans match. Truncated=true means the trace exceeded the per-trace span cap and the spans returned are an early prefix.
| Name | Required | Description | Default |
|---|---|---|---|
| traceId | Yes | Trace id (32-character lowercase hex). |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With readOnlyHint/idempotentHint already covering the safety profile, the description adds real behavioral context: failure on no matches, the per-trace span cap, and the semantics of truncated=true (early prefix). It stops short of naming the cap value or suggesting a follow-up when truncated, keeping it from a 5.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three tight sentences: what is returned, what failure looks like, then the truncation caveat. No filler and the core behavior is front-loaded.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a single-parameter read tool with no output schema, the description covers return contents (spans plus service.name), the error case, and the truncation edge case. Minor gaps remain around the cap size and span ordering, but nothing critical to calling it correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% and the single traceId parameter is fully documented in the schema (32-char lowercase hex). The description adds nothing beyond 'the given trace id', so the baseline 3 applies — the schema does all the work here.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description uses a specific verb+resource ('Return every span belonging to the given trace id') and additionally states the enrichment ('each span's resource service.name'). This clearly separates it from query_traces or list_trace_services, though it never names those siblings explicitly.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is implied by the required traceId ('the given trace id'), and the error condition when no spans match is stated. However, there is no explicit guidance on when to prefer this over query_traces for trace lookup, nor any prerequisites mentioned.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_log_servicesARead-onlyIdempotent
Distinct service.name values attached to log records inside the given UTC window. Sorted alphabetically.
| Name | Required | Description | Default |
|---|---|---|---|
| to | Yes | Window end (ISO-8601 UTC). Required, > from. | |
| from | Yes | Window start (ISO-8601 UTC). Required. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover readOnlyHint and idempotentHint, so the bar is lower, and the description still adds genuinely useful behavior: results are de-duplicated ('distinct') and deterministically ordered ('sorted alphabetically'). It omits edge-case behavior such as what happens with an empty window or whether the list is capped, keeping it short of a 5.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two short sentences, front-loaded with what is returned before the ordering detail. Every clause carries information; nothing is repeated from the schema or annotations.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a two-parameter read-only listing tool with no output schema, the description effectively characterizes the return value (distinct service names, alphabetical) so an agent knows what it gets. Minor gaps around empty-result and list-size behavior are the only missing pieces.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% and both parameters are fully documented (ISO-8601 UTC, to > from). The description only restates that the pair forms a UTC window, adding little beyond the schema, so the baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states the exact resource ('distinct service.name values') and scopes it to log records inside a UTC window, which implicitly separates it from list_metric_services and list_trace_services. It doesn't explicitly name those siblings, so differentiation is inferable rather than stated.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is only implied: the window scoping suggests this is a discovery/enumeration tool to run before query_logs, but the description never says when to use it, when not to, or what alternative to pick. An agent must infer the discovery workflow from the return shape alone.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_metricsARead-onlyIdempotent
Every instrument currently in the store, with point count and originating service. The four-field key (resourceHash, scopeName, name, kind) identifies a single time-series for query_metric_points.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The readOnlyHint and idempotentHint annotations already establish the safety profile. The description adds useful return-value context by naming the point count, originating service, and key fields, but does not cover pagination, ordering, or scale considerations for enumerating all instruments.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two tightly written sentences front-load the tool's purpose and then explain the key linkage to query_metric_points. Every sentence carries useful information without repetition.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a zero-parameter listing tool without an output schema, the description does a good job explaining the returned fields and the key structure. It could be slightly more complete by mentioning pagination or whether the full store is always returned, but the core context is present.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
This tool has zero input parameters, so there is no parameter semantics to explain. The baseline score for a parameterless tool applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states that this tool returns every instrument in the store along with point count and originating service, giving a specific resource and return content. It connects to query_metric_points but does not explicitly distinguish itself from list_metric_services, so sibling differentiation is incomplete.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The second sentence implies that the returned four-field key is used by query_metric_points, which gives some usage context. However, it does not explicitly say when to use this tool versus list_metric_services or query_metric_points, leaving the guidance implied rather than stated.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_metric_servicesARead-onlyIdempotent
Distinct service.name values across the recorded instruments. Sorted alphabetically.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint and idempotentHint, so safety and repeatability are covered. The description adds one useful behavioral detail beyond annotations: results are sorted alphabetically. It does not describe return shape, empty-result behavior, or any scoping/pagination behavior.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two short sentences, both front-loaded with the essential output description and ordering. No filler or repetition; every phrase contributes information.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a zero-parameter, read-only list tool with annotations and no output schema, the description is nearly complete: it tells the agent exactly what values come back and how they are ordered. The only minor gap is that it relies on 'recorded instruments' rather than explicitly naming metrics.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
This tool takes zero parameters, so the baseline is 4. There are no parameter semantics to explain, and the schema is empty as expected for a pure enumeration endpoint.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states the resource precisely: distinct service.name values, with the source identified as 'recorded instruments' and the output ordering given. It is distinguishable from list_log_services and list_trace_services by its instrument/metric framing, though it never explicitly says 'metrics' and omits a leading verb.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
There is no when-to-use guidance, no prerequisite context, and no routing to alternatives such as list_log_services, list_trace_services, or query_metric_points. The intended use is only implied by the tool name and resource description.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_trace_servicesARead-onlyIdempotent
Distinct service.name values attached to traces whose earliest span falls inside the given UTC window. Sorted alphabetically.
| Name | Required | Description | Default |
|---|---|---|---|
| to | Yes | Window end (ISO-8601 UTC). Required, > from. | |
| from | Yes | Window start (ISO-8601 UTC). Required. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint and idempotentHint, so safety is covered. The description adds real behavioral context beyond that: results are distinct service.name values, the window is evaluated against each trace's earliest span, and output is alphabetically sorted.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences, zero waste. The core operation is front-loaded and the ordering guarantee follows as a short trailing clause.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With no output schema, the description compensates by describing what comes back (distinct service.name values, alphabetically sorted). The only minor gap is that no sibling or pagination/edge-case guidance is included, but nothing essential to correct invocation is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so both params are documented and a 3 baseline applies. The description goes beyond that by clarifying that the from/to window filters on the earliest span timestamp rather than any span, which is meaningful semantic information not present in the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb+resource: distinct service.name values attached to traces, with the window defined by earliest span and an ordering guarantee. The word 'traces' implicitly distinguishes it from list_log_services and list_metric_services, though those siblings are never named explicitly.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The scope statement ('traces whose earliest span falls inside the given UTC window') implies when the tool applies, but there is no explicit when-to-use or when-not guidance and no reference to query_traces or the sibling service-list tools as alternatives.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
query_logsARead-onlyIdempotent
List log records inside a UTC time window, ordered by timestamp descending. Supports keyset pagination via the 'cursor' returned in 'nextCursor'.
| Name | Required | Description | Default |
|---|---|---|---|
| to | Yes | Window end (ISO-8601 UTC). Must be strictly greater than 'from'. Required. | |
| from | Yes | Window start (ISO-8601 UTC, e.g. 2026-04-23T12:00:00Z). Required. | |
| limit | No | Maximum records returned. Defaults to QueryApi.DefaultLimit; capped at QueryApi.MaxLimit. | |
| cursor | No | Opaque continuation token returned by a previous call. Pass to fetch the next page. | |
| service | No | Filter by service.name (exact match). | |
| traceId | No | Filter by trace id (32-char lowercase hex). | |
| minSeverity | No | Filter records with severity_number >= this value (OTLP 0-24). |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly and idempotent, so the safety profile is covered. The description adds non-obvious behavior: results are ordered by timestamp descending and pagination is keyset-based via a 'nextCursor' token, which is genuinely useful beyond the annotations. It stops short of stating the concrete default/max limit values or any rate limits.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two tight sentences with zero filler; the ordering guarantee is front-loaded before the pagination note. Every clause carries information an agent needs.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With no output schema the description usefully names the 'nextCursor' field, which is the one return detail an agent must know to page. For a 7-parameter filtered query tool this is nearly sufficient, though a note on default page size or the existence of service/trace/severity filters would close the gap.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so every parameter including cursor, limit and the severity filter is already documented in the schema. The description only echoes the cursor contract already present, adding no new per-parameter semantics. Baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('List log records') scoped to a UTC time window with an explicit ordering. The resource noun separates it from query_traces and query_metric_points, though it never names those siblings directly.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is implied: call this to enumerate log records in a window. It explains pagination mechanics but gives no explicit when-to-use versus query_traces or when to reach for list_log_services instead of filtering by service here.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
query_metric_pointsARead-onlyIdempotent
Return the points of a single instrument time-series identified by (resourceHash, scopeName, instrumentName, kind) inside the required UTC window. Use list_metrics first to discover the four-field key. Truncated=true means the window contains more points than the configured cap and the points returned are an early prefix.
| Name | Required | Description | Default |
|---|---|---|---|
| to | Yes | Window end (ISO-8601 UTC). Required, > from. | |
| from | Yes | Window start (ISO-8601 UTC). Required. | |
| kind | Yes | Instrument kind: Gauge, Sum, Histogram, ExponentialHistogram, Summary. | |
| scopeName | Yes | Scope name (use empty string for the anonymous scope). | |
| resourceHash | Yes | Resource hash (lowercase hex) — see list_metrics output. | |
| instrumentName | Yes | Instrument name. | |
| includeAttributes | No | Set true to hydrate per-point attribute maps. Default false (cheaper for single-value widgets). |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations establish readOnly/idempotent, and the description adds genuinely non-obvious behavior: a server-side cap can truncate results, and truncated=true means the returned points are an early prefix rather than the full window. That is the key caveat an agent needs before drawing conclusions from partial data. It does not mention ordering or what a point object looks like, keeping it from a 5.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three sentences, front-loaded with what is returned and the identity key, then the prerequisite, then the truncation caveat. No filler or restatement of the tool name.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With no output schema, the description still covers the essential return semantics (points plus a truncation flag) and the discovery prerequisite. Minor gap: it says nothing about point ordering or how to page past a truncated window, which an agent would need for complete coverage of a capped result set.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% and each parameter already carries a description including enum-free kind values and the empty-string anonymous scope. The description adds only the framing that the four fields together form a key and that the window must be UTC — modest value on top of a fully documented schema, so the baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Specific verb and resource: 'return the points of a single instrument time-series', with the exact composite key that identifies it. It is easily distinguished from list_metrics (discovery) and from the query_logs/query_traces siblings, which target different signal types.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
'Use list_metrics first to discover the four-field key' names the prerequisite discovery step and the sibling that supplies it. No explicit when-not-to-use guidance, but for a lookup tool gated on a composite key the sequencing instruction covers the main misuse risk.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
query_tracesARead-onlyIdempotent
List trace summaries inside a UTC time window, ordered by start time descending. Supports keyset pagination via 'cursor'/'nextCursor'.
| Name | Required | Description | Default |
|---|---|---|---|
| to | Yes | Window end (ISO-8601 UTC). Required, > from. | |
| from | Yes | Window start (ISO-8601 UTC). Required. | |
| limit | No | Maximum traces returned. Defaults to QueryApi.DefaultLimit; capped at QueryApi.MaxLimit. | |
| cursor | No | Opaque continuation token from a previous call. | |
| service | No | Filter by service.name (exact match on root span's resource). |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover the safety profile (readOnly, idempotent), and the description adds real behavior: deterministic ordering by start time descending and a keyset pagination contract. It still omits whether the window is inclusive/exclusive and that results are summaries rather than full spans, but it adds meaningful context beyond the annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences, zero filler, with the core purpose and ordering front-loaded before the pagination detail. Every clause carries information an agent needs.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
There is no output schema, and the description partially compensates by naming the summary return type and the nextCursor field. Combined with the fully documented input schema, an agent has enough to call it correctly; only window-boundary semantics are left unstated.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so baseline is 3, but the description adds value by pairing the input 'cursor' with a 'nextCursor' continuation token that exists only in the response, clarifying the pagination loop. It does not restate the limit defaults or service filter semantics, which the schema already handles.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('List trace summaries'), the scoping condition (UTC time window), and the sort order (start time descending). It is clearly distinct from get_trace (single trace) and list_trace_services, though no sibling is named explicitly.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is only implied: the tool is for browsing traces within a window. There is no explicit when-to-use/when-not-to-use guidance and no pointer to get_trace for full trace detail, which is the obvious companion. The pagination sentence does signal a follow-up call pattern.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
8 tool updates
- First observed
get_trace - First observed
list_log_services - First observed
list_metric_services - First observed
list_metrics - First observed
list_trace_services - First observed
query_logs - First observed
query_metric_points - First observed
query_traces
TDQS
Scored across 8 tools
Each tool has a distinct signal+action scope: listing services per telemetry type, querying records/points per signal, and fetching a single trace by id. The only potential overlap between list_metrics and query_metric_points is clearly resolved by the descriptions: one discovers instruments, the other reads points for a specific series.
All names use a consistent snake_case verb_noun pattern (get_trace, list_*_services, query_logs, query_traces, query_metric_points). The get/list/query verbs are used coherently for detail, discovery, and time-window retrieval.
Eight tools is well-scoped for a read-only OTLP observability dashboard covering logs, metrics, and traces. Each tool maps to a clear part of the discovery/query workflow without obvious filler.
The surface covers service discovery and querying for all three telemetry signals, plus trace detail retrieval. A notable minor gap is that query_logs and query_traces appear to support only time-window queries, with no service filter despite dedicated service-listing tools, so agents may need to scan and filter client-side.
Maintenance
Related MCP Connectors
Query Honeycomb observability data: traces, events, metrics, SLOs, triggers, and boards.
Query Telemetry Machine observability data and instrument your apps with OpenTelemetry.
Open-source agent that observes and fixes your application. Query logs, traces, metrics, incidents.
Query Checkly synthetic monitoring — checks, statuses, results, alerts, reporting and dashboards.
Related MCP Servers
- FlicenseAqualityDmaintenanceEnables querying OpenTelemetry logs stored in OpenSearch across development and production environments. Provides tools for searching logs by various criteria including free-text Lucene queries, trace IDs, service names, error levels, and specific fields.8-
- AlicenseNot gradedqualityDmaintenanceEnables natural language querying and analysis of OpenTelemetry traces, metrics, and logs stored in Elasticsearch/OpenSearch, allowing AI assistants to investigate performance issues, find root causes, and explore system behavior.3 npm14MIT
- AlicenseNot gradedqualityCmaintenanceProvides read-only access to Loki, Prometheus, and Tempo APIs, enabling natural language queries for logs, metrics, and traces. Supports multiple instances and authentication via bearer tokens.1MIT
- AlicenseBqualityDmaintenanceEnables querying Grafana Tempo traces and spans with flexible filtering, including TraceQL queries, tag-based filters, duration ranges, and aggregated statistics.67 npmGPL 3.0