Grafana
Query a Grafana from an agent: dashboards, Prometheus, Loki, alert rules,
incidents, and — through grafana_api_request — anything else its HTTP API
serves. The server is Grafana Labs’ own
grafana/mcp-grafana, pinned at
v0.17.1, running inside strad’s bundle image.
That last part is the whole design. Everything else on this page follows from one fact: Grafana ships this server as a Go binary and a Docker image, and as nothing else.
Why it is baked into the image
Section titled “Why it is baked into the image”No strad server kind runs a local command. There is no
command: in the config schema, deliberately: a gateway whose config can spawn
processes on the box is a different security posture than one that cannot.
So a server with no npm package has two honest options: front it with a
remote-http slug and host it somewhere, or put it in the image. strad does
the second. servers/bundle/Dockerfile fetches the pinned release, verifies its
SHA-256 against a digest committed in this repo, and installs it at
/usr/local/bin/mcp-grafana. servers/grafana/ is the other half: a small MCP
proxy that supervises the binary as a stdio child process and forwards
tools/list and tools/call to it.
From the outside it is an ordinary mount. A slug names path: /grafana-registry,
strad’s gateway proxies to it over the private network, and nothing in the config
knows a Go binary is involved.
What this replaced
Section titled “What this replaced”The same server used to run agent-side, as a ~900-character sh -c in every
client’s MCP config that curled the pinned release into $TMPDIR, verified it,
cached it and exec’d it — on every fresh session, on every machine. Two things
were wrong with that, and neither was the shell script:
- The credential lived in the client. A Grafana service-account token was an
environment variable on whatever ran the agent, which is exactly the thing
strad’s parameter store exists to stop. Hosted here, the token is a
${GRAFANA_REGISTRY_SERVICE_ACCOUNT_TOKEN}reference resolved at render time and injected onto one container. - Every machine re-downloaded the binary. Baked into the image, it is pulled once per deploy.
Read-only at the binary
Section titled “Read-only at the binary”The child process runs with two flags, and they are not adjustable per slug:
-disable-writeremoves the 13 write tools (create/update/delete of dashboards, datasources, folders, annotations, incidents and snapshots) and narrows the alerting tools to their read actions. 65 tools become 52.-disable-proxiedremoves “proxied” tools — toolsmcp-grafanadiscovers at startup from MCP servers advertised by the Grafana’s own datasources. Their names, schemas and side effects are authored by whoever administers that Grafana,-disable-writedoes not constrain them, and a surface that changes when a datasource does cannot be enumerated by the pre-deploy tool-policy gate.
This is a deliberate exception to how capability works everywhere else in the bundle. Every other upstream in that image runs at full capability and strad’s gateway withholds tools per slug (Concepts). A gateway policy can only ever withhold a tool; a flag at the binary means the tool is not in the process. For a fleet’s observability control plane — where a wrong write is an alert rule or a dashboard nobody notices is gone — the stronger guarantee is worth the inconsistency.
The consequence is stated rather than hidden: a read-write Grafana is a second mount, never this one with a flag removed.
One mount, more than one Grafana
Section titled “One mount, more than one Grafana”A Grafana service-account token is one Grafana, the same way an OAuth refresh token is one Google account. So the mount takes its origin and its credential from the environment, and the bundle host stands up one instance — and one child process — per slug that the container carries a complete credential for.
| variable | what it is |
|---|---|
GRAFANA_URL | the Grafana origin, e.g. https://grafana.prod.registry.modelcontextprotocol.io |
GRAFANA_SERVICE_ACCOUNT_TOKEN | a service-account token; both are required for a per-slug instance |
GRAFANA_ORG_ID | optional, for a Grafana with more than one organisation |
A slug on path: /<its own slug> is handed its own values under
<SLUG>__<NAME>; that is the general
per-slug variable mechanism, and Grafana is one of the four
trees that reads it.
The conventional /grafana mount is always served, so the slug and its 52
tools are visible in the console before any credential exists. Be precise about
what it holds, though: the renderer emits every supplementary server’s variables
under the bare name as well as the per-slug one, and an instance with no
per-slug names of its own falls back to the bare ones. So on a container where
grafana-registry is the only Grafana slug, /grafana comes up holding that
slug’s URL and token rather than degraded. Nothing routes to it — no config entry
names path: /grafana — but it is a live mount, not an empty one, and a
deployment that wants /grafana to mean obs.tadasant.com should say so with its
own grafana slug rather than rely on the fallback.
Which means adding a second Grafana is config alone: a new slug, its own
path: /<slug>, its own two variables. No image change.
The child gets an allowlisted environment
Section titled “The child gets an allowlisted environment”Every other mount in that image is code inside the host process. This one is a
separate executable, and a child spawned with the default environment inherits
the whole container’s: a Slack bot token, a 1Password service-account token, a
GCP service-account key, six Google OAuth refresh tokens. mcp-grafana needs none
of them, and it has a tool that makes arbitrary HTTP requests to a host somebody
else configures.
So the child is given an explicit list and nothing else: the three Grafana
variables above, plus PATH, HOME, TMPDIR and the two SSL_CERT_* names Go
reads for a private CA. OTEL_* is deliberately excluded — mcp-grafana can
export its own traces and logs, and those are records strad did not author
arriving outside the telemetry allowlist.
Not configured is a supported state
Section titled “Not configured is a supported state”A mount with no credential still lists its 52 tools — mcp-grafana builds
its tool surface without contacting Grafana at all — and reports itself
degraded on the bundle’s /healthz, naming the variables it is missing. Only
calls fail. One unconfigured server must never cost the other twenty mounts their
boot, and a slug that cannot be seen is a slug nobody can seed.
The same applies when the binary itself cannot be started: the mount degrades, the container keeps serving, and the next call retries — a child that was OOM-killed comes back rather than staying dead until a redeploy.
The staging slug
Section titled “The staging slug”grafana-registry points at
https://grafana.prod.registry.modelcontextprotocol.io — the observability plane
for the MCP Registry’s own GKE cluster (VictoriaMetrics + VictoriaLogs + an
OpenTelemetry collector). It is a different Grafana from obs.tadasant.com,
which is the dashboard plane for the Tadas/Zimmer/strad fleet: use this one for
the registry’s own traffic, and nothing else.
It ships enabled: false until its token is seeded, like every entry whose
secret is not in the store yet — an unseeded ${NAME} fails the whole
render, not just
its own slug. Minting that token needs admin on the registry’s Grafana, whose
only interactive login is Google OAuth restricted to @modelcontextprotocol.io,
so it comes from an MCP Registry maintainer rather than from a tadasant.com
account.
Two things are worth knowing before querying it:
- Its datasources are provisioned without pinned UIDs, so the VictoriaLogs
UID is a Grafana-generated string rather than the friendly
VictoriaLogsthatobs.tadasant.comuses. Calllist_datasourcesfirst. - There is no VictoriaLogs MCP server. Logs come through
grafana_api_requestagainst the datasource proxy, e.g.GET /api/datasources/proxy/uid/<UID>/select/logsql/query?query=*&start=15m&limit=100.
Version bumps
Section titled “Version bumps”The pin lives in servers/bundle/Dockerfile as MCP_GRAFANA_VERSION plus a
SHA-256 digest per architecture. Bumping it means changing all three; a digest
that does not match fails the build rather than shipping. If the release changes
the read surface, the bundle smoke test’s grafana: 52 fails CI — which is the
point of asserting a count nobody can predict from reading the diff. See
Limitations #68.