@mgcrea/mcp-grafana
# @mgcrea/mcp-grafana
A [Model Context Protocol](https://modelcontextprotocol.io) server for the
[Grafana HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/): dashboards and
the data their panels show, PromQL and LogQL through Grafana's datasources, alerts, alert rules,
silences and annotations, shaped to fit a context window.
The server is **read-only by default**. The write tools are not merely refused when writes are
off — they are never registered, so an agent cannot call them at all.
## What it does that the API does not
- **Runs a panel the way the dashboard does.** `grafana_run_panel_query` fills in template
variables before sending the query, which `/api/ds/query` will not do for you. It takes them
from `vars`, else the dashboard's saved value, else what the browser would pick: All, or the
first value of a `label_values(...)` query, or `""` ("None") when that query has no values.
That last case is common, not an edge: provisioned dashboards (every kube-prometheus-stack
mixin) are saved with no current values, and on a single-cluster install the mixins' `cluster`
variable has no values at all. Mixed panels, datasource variables, hidden queries,
expressions and library panels are handled. The result says which values were used and
what query was sent.
- **Shapes data frames.** Grafana answers every datasource in columnar frames: one frame per
Prometheus series, each repeating its schema, and Loki log rows that each repeat the full
stream label set alongside three UI-only columns. Here, time series come back on one step grid
with min/max/avg/last, labels shared by every series or line come back once as
`common_labels`, log lines are cut at `max_line_chars`, and anything else is a table.
- **Summarizes dashboards.** `grafana_get_dashboard` flattens rows (collapsed, expanded and
pre-schema-16), and lists each panel's id, type, unit and queries without the layout and
styling: about half the JSON on the kubernetes mixins. `view=full` is there for editing.
- **Reads alerting from everywhere it lives.** Grafana-managed rules and Grafana's own
Alertmanager, plus every Prometheus/Loki datasource's ruler and every Alertmanager
datasource. A kube-prometheus-stack install usually has everything in the datasources and
nothing in Grafana. A source that does not answer (a Loki with no ruler) shows up in
`unavailable` and does not fail the call.
- **Says which org it is reading.** A service account token is bound to one org and takes no
org in the request, so a token copied from the wrong org answers everything with plausible
data. The org is shown in the startup banner and in `grafana_auth_status`, and stamped on every
data response. With `GRAFANA_ORG_ID` set, every data tool refuses on a mismatch.
## Tools
| Tool | What it does | Writes |
| --------------------------- | ------------------------------------------------------------------------------------ | ------ |
| `grafana_auth_status` | URL, version, org, login, effective role and permissions; setup steps when unset | no |
| `grafana_search` | Dashboards and folders by title, tag, folder, starred | no |
| `grafana_get_dashboard` | Summary (variables, panels, queries), one panel's JSON, or the full JSON | no |
| `grafana_run_panel_query` | A panel's queries with variables filled in, and the data they return | no |
| `grafana_query_prometheus` | PromQL, instant or range, through a Prometheus datasource | no |
| `grafana_query_loki` | LogQL log lines or metric queries through a Loki datasource | no |
| `grafana_query` | Raw queries against any datasource, including server-side expressions | no |
| `grafana_list_labels` | Label names or values for Prometheus/Loki, restricted by selector, last hour | no |
| `grafana_list_datasources` | uid, name, type, default — never credentials | no |
| `grafana_list_alerts` | Active alerts from every Alertmanager, most severe first | no |
| `grafana_list_alert_rules` | Rules and their state from Grafana and every datasource ruler | no |
| `grafana_list_silences` | Silences with readable matchers | no |
| `grafana_list_annotations` | Deploys, incidents and alert state changes in a window | no |
| `grafana_request` | GET any `/api/` or `/apis/` endpoint; any method (with `confirm`) when writes are on | gated |
| `grafana_save_dashboard` | Create or update a dashboard; refuses to overwrite a concurrent edit | yes |
| `grafana_delete_dashboard` | Delete a dashboard (`confirm: true`) | yes |
| `grafana_create_annotation` | Mark a moment or region | yes |
| `grafana_create_silence` | Silence alerts matching matchers, for a duration | yes |
| `grafana_expire_silence` | End a silence (`confirm: true`) | yes |
## Configure
| Variable | Required | Description |
| ------------------------------- | -------- | ------------------------------------------------------------------------------------------- |
| `GRAFANA_URL` | yes | Grafana root URL, including any sub-path, e.g. `https://grafana.example.com`. |
| `GRAFANA_SERVICE_ACCOUNT_TOKEN` | one of | A service account token (`glsa_…`). `GRAFANA_API_KEY` is accepted as an alias. |
| `GRAFANA_USERNAME` / `PASSWORD` | one of | Basic auth instead of a token. |
| `GRAFANA_ORG_ID` | no | The org this server must read. Sent as `X-Grafana-Org-Id`; data tools refuse on a mismatch. |
| `GRAFANA_ALLOW_WRITES` | no | `1` to register the write tools. |
| `GRAFANA_HEADERS` | no | Extra headers as JSON, e.g. a Cloudflare Access service token. |
| `GRAFANA_TIMEOUT` | no | Seconds per request. Defaults to 30. |
| `GRAFANA_MAX_RETRIES` | no | Retries on 429/502/503/504 for reads. Defaults to 2. Writes are never retried. |
| `GRAFANA_DEBUG` | no | `1` to log each request to stderr. |
The names match Grafana's own `mcp-grafana`, so swapping is a change of `command` in an
`.mcp.json`.
A token needs **Viewer** for every read tool (with Viewer, the datasource list comes from the
frontend settings, since `/api/datasources` needs more), **Editor** for dashboards and
annotations, and whatever your alerting setup requires for silences. `grafana_auth_status` reports
the role and the permissions the tools depend on.
## Security
- **Read-only by default.** `GRAFANA_ALLOW_WRITES=1` adds the write tools; the destructive ones
then additionally require an explicit `confirm: true` on every call, and `grafana_request`
requires it for any method but GET.
- **Saves never overwrite.** `grafana_save_dashboard` sends `overwrite: false`, so a dashboard
changed since it was read is refused, not clobbered; every save stays in the version history.
- **No runtime dependencies** beyond the MCP SDK and Zod — native `fetch` throughout.
- **Credentials stay in your environment**, are never echoed back (`grafana_auth_status` lists
header names, not values), and are refused when embedded in `GRAFANA_URL`.
## Quick start
```bash
pnpm install
pnpm build
```
```json
{
"mcpServers": {
"grafana": {
"command": "npx",
"args": ["-y", "@mgcrea/mcp-grafana"],
"env": {
"GRAFANA_URL": "https://grafana.example.com",
"GRAFANA_SERVICE_ACCOUNT_TOKEN": "glsa_..."
}
}
}
}
```
### Inspect the tools
```bash
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"cli","version":"0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
| node dist/cli.js 2>/dev/null | jq -r '.result.tools[]?.name'
```
## Develop
```bash
pnpm dev # tsdown --watch
pnpm test # vitest
pnpm typecheck
pnpm lint
pnpm format
```
### Publish
```bash
pnpm dlx release-it # bump, commit, tag
git push --follow-tags # CI publishes to npm + GHCR from the tag
```
## License
MIT
TDQS
Scored across 14 tools
Most tools target a clearly distinct resource or action (list_labels vs list_annotations vs list_silences vs get_dashboard vs run_panel_query), and the descriptions explicitly disambiguate the risky pairs: grafana_query tells you to prefer grafana_query_prometheus/grafana_query_loki, and grafana_list_alert_rules points to grafana_list_alerts for instances. The residual overlaps are grafana_query (generic /api/ds/query) versus the two datasource-specific query tools, and grafana_request acting as a catch-all over nearly everything. Both are framed as escape hatches, which mitigates but does not eliminate the risk of misselection.
There is a single grafana_ prefix and a clear verb_noun cadence: list_* (labels, datasources, alerts, alert_rules, silences, annotations), query_* (prometheus, loki, and bare query), get_dashboard, run_panel_query, search. The deviations are small but real: grafana_search omits its object (search_dashboards would fit the pattern), grafana_auth_status is noun-only with no verb, and grafana_query / grafana_request carry no target noun.
Fourteen tools is squarely in the well-scoped band, and each one earns its place: datasource discovery, label discovery, three query paths (Prometheus, Loki, generic), dashboard read plus panel replay, four alerting-state readers, annotations, auth check, and one read-only HTTP escape hatch. Nothing looks padded, and the escape hatch keeps the surface from ballooning into a tool per API endpoint.
The read-side lifecycle for Grafana investigation is covered end to end: discover datasources and labels, query metrics/logs/arbitrary sources, find and read dashboards, re-run panel queries with variables filled, and inspect alerts, rules, silences and annotations. The gaps are real but workaroundable: the surface is read-only, and grafana_get_dashboard refers to a grafana_save_dashboard tool that is not registered, which is a dangling pointer an agent may chase.