Skip to main content
Glama
mgcrea

@mgcrea/mcp-grafana

by mgcrea
README.md
# @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

A4.2/5.0

Scored across 14 tools

Disambiguation4/5

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.

Naming Consistency4/5

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.

Tool Count5/5

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.

Completeness4/5

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.

Maintenance

ActivityMaintained
ResponsivenessNo issues