Skip to main content
Glama
stonoyan04

grafana-mcp-server

by stonoyan04
README.md
# Grafana MCP Server

A [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server that gives AI assistants read-only access to everything your Grafana can already see — Prometheus/Loki metrics, ClickHouse/Postgres/MySQL datasources, and the dashboards your team has already built. Ask questions in natural language and get answers backed by real data.

Works with [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Claude Desktop](https://claude.ai/download), and any MCP-compatible client. Your project can be in any language — this server runs independently.

<p align="center">
  <img src="https://img.shields.io/badge/MCP-compatible-blue" alt="MCP Compatible" />
  <img src="https://img.shields.io/badge/node-%3E%3D18-green" alt="Node >= 18" />
  <img src="https://img.shields.io/badge/license-MIT-brightgreen" alt="MIT License" />
</p>

## How It Works

```
Your project (any language)     grafana-mcp-server            Grafana            Datasources
┌───────────────────┐          ┌──────────────────┐          ┌──────────┐       ┌────────────┐
│  Claude Code or   │  stdio   │  Builds the      │  HTTPS   │ /api/ds/ │──────>│ Prometheus │
│  Claude Desktop   │────MCP──>│  per-plugin      │────────->│  query   │──────>│ ClickHouse │
│                   │<─────────│  query model     │<─────────│          │──────>│ Postgres … │
└───────────────────┘          └──────────────────┘          └──────────┘       └────────────┘
```

The server talks only to Grafana's HTTP API. Grafana proxies the query to the datasource with **its** credentials, so the assistant never holds a database password, and whatever the Grafana user can see is exactly what the assistant can see — no more. All requests are **read-only** (use a Viewer credential to make that a guarantee).

## Requirements

- **Node.js** >= 18 **or Docker** (only for running this MCP server)
- A Grafana account you can sign in with — Google / SSO / password, whatever your Grafana uses. **No admin rights, no service account, no API token required**: `grafana-mcp-server login` signs you in in a browser and hands the session to the server (see [Authentication](#authentication)). A service-account token works too if you have one.
- **Chrome or Edge installed** (used only as the sign-in window; your daily browser can be anything). If neither is present, `login` falls back to a guided cookie copy in your default browser.

## Quick Start

### Option A: Node.js

#### 1. Clone and build

```bash
git clone https://github.com/stonoyan04/grafana-mcp-server.git
cd grafana-mcp-server
npm install
npm run build
```

This creates the compiled server at `dist/main.js`.

#### 2. Sign in

```bash
GRAFANA_URL=https://grafana.example.com npm run login
```

**If Chrome or Edge is installed** (as the default browser, or just present on the machine — that covers almost everyone, whatever their daily browser), `login` opens a dedicated window of it — its own throwaway profile, not your everyday one — on the Grafana login page. Sign in exactly as you always do. The moment Grafana issues a session, the command stores it, removes it from that profile, verifies it, and prints:

```
[grafana-mcp-server] ✓ signed in as jane <jane@example.com>
```

No pasting, no token, nothing secret in a config file. If Grafana is behind Cloudflare Access you sign in to that in the same window, and its cookie is captured too. Don't close the window until you see the ✓. The session is stored at `~/.grafana-mcp/sessions/<host>.json` (mode 600).

> Playwright can only drive Chrome and Edge (Arc, for one, can't be automated at all), so `login` uses one of those as the sign-in window regardless of your default browser — your default browser is never touched. **Only if neither Chrome nor Edge is installed** does `login` fall back to opening Grafana in your default browser and guiding a one-time `grafana_session` copy from DevTools.

> **Why a dedicated window and not my open tab?** A browser never hands its cookies to a command-line tool — that isolation is the point of a browser — so `login` drives an instance it controls, in a separate profile, and takes the session out of it.

> **Force a specific browser:** `npm run login -- --browser chrome` (or `msedge`, `chromium` after `npx playwright install chromium`, or a path to a Chromium binary). `npm run login -- --paste` reads a cookie from stdin with no browser opened.

#### 3. Configure

**Claude Code** — one command, or the equivalent `.mcp.json` block:

```bash
claude mcp add grafana --scope user --env GRAFANA_URL=https://grafana.example.com -- node /home/john/grafana-mcp-server/dist/main.js
```

```json
{
  "mcpServers": {
    "grafana": {
      "command": "node",
      "args": ["/home/john/grafana-mcp-server/dist/main.js"],
      "env": {
        "GRAFANA_URL": "https://grafana.example.com"
      }
    }
  }
}
```

**Claude Desktop** — same block in the config file:
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`

> **Note:** Replace `/home/john/grafana-mcp-server` with the actual path where you cloned this repo. The only required setting is `GRAFANA_URL` — the credential comes from the session `login` stored. Restart the client after adding the server.

> **Have a service-account token instead?** Add `"GRAFANA_TOKEN": "glsa_…"` to `env` and skip `login`. Or point `GRAFANA_CREDENTIAL_FILE` at a `0600` file holding it, or inject it from a secret manager at launch (e.g. `charter secret exec … --exec -- node dist/main.js`).

### Option B: Docker

```bash
git clone https://github.com/stonoyan04/grafana-mcp-server.git
cd grafana-mcp-server
docker build -t grafana-mcp-server .
```

```json
{
  "mcpServers": {
    "grafana": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-v", "/home/john/.grafana-mcp:/root/.grafana-mcp",
        "-e", "GRAFANA_URL=https://grafana.example.com",
        "grafana-mcp-server"
      ]
    }
  }
}
```

Run `login` on the **host** (`GRAFANA_URL=… npm run login` from a Node checkout, since the container has no browser) and mount `~/.grafana-mcp` read-write as shown — the container needs to write rotated sessions back. Or skip the mount and pass `-e GRAFANA_TOKEN=glsa_…` if you have a service-account token.

> **Why `-i` and no `-t`?** MCP speaks JSON over stdin/stdout. `-i` keeps stdin open; a TTY (`-t`) would corrupt the stream.

If Grafana is behind Cloudflare Access, mount your host's token cache read-only — `-v /home/john/.cloudflared:/root/.cloudflared:ro` — and run `cloudflared access login https://grafana.example.com` on the host first. The container cannot run `cloudflared` itself, so it reads the mounted cache (or a `CF_ACCESS_TOKEN`); when the token expires, re-run the login on the host.

### 4. Discover your datasources

```
List the Grafana datasources
```

Every query tool needs a datasource `uid`, and the datasource **type** decides which tool to use — `query_sql` for ClickHouse/Postgres/MySQL, `query_metrics` for Prometheus/Loki. `list_datasources` tells you both.

### 5. Start asking questions

```
How many messages per Kafka topic were produced in the last 7 days?   → query_metrics on Prometheus
Which dashboards mention "kafka"?                                     → search_dashboards
Show me the panel queries in that dashboard                           → get_dashboard
Run: SELECT count() FROM events WHERE created > now() - INTERVAL 1 DAY → query_sql on ClickHouse
```

> **Tip:** `get_dashboard` returns the raw SQL / PromQL behind each panel. Reusing a query a human already wrote and trusts beats writing one from scratch.

## Tools

The server exposes 6 read-only tools:

### `list_datasources`

List datasources with `uid`, `name`, `type`, and which query tool applies. No parameters.

### `query_sql`

Run raw SQL against a SQL-family datasource through Grafana and get rows back.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `datasourceUid` | string | | Datasource uid or exact name |
| `sql` | string | | The SQL to run |
| `from` | string | `now-6h` | Range start (for `$__timeFilter`-style macros) |
| `to` | string | `now` | Range end |

The query object sent to `/api/ds/query` is built per plugin — `grafana-clickhouse-datasource`, `vertamedia-clickhouse-datasource`, and the built-in Postgres/MySQL/MSSQL all want different shapes.

### `query_metrics`

Run a PromQL or LogQL expression. Omit `from` for an instant query.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `datasourceUid` | string | | Datasource uid or exact name |
| `expr` | string | | PromQL / LogQL expression |
| `from` | string | | Range start, e.g. `now-24h`. Omit for an instant query. |
| `to` | string | `now` | Range end / evaluation time |
| `stepSeconds` | number | `300` | Range-query step |
| `maxDataPoints` | number | `1000` | Points per series cap |

### `search_dashboards`

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `query` | string | | Title substring |
| `tag` | string | | Dashboard tag |
| `limit` | number | `20` | Max results |

### `get_dashboard`

| Parameter | Type | Description |
|-----------|------|-------------|
| `uid` | string | Dashboard uid from `search_dashboards` |

Returns the panels (rows flattened) with each panel's datasource and raw queries, plus the dashboard's template variables and default time range.

### `health`

Checks connectivity and **tells the two auth layers apart**: `failingLayer: "cloudflare"` means run `cloudflared access login` (or `login` again); `failingLayer: "grafana"` means the stored session/credential is dead or under-privileged — run `grafana-mcp-server login`. Reports who the credential authenticates as and how many datasources it can see. No parameters. Call it first whenever another tool fails.

Data frames are flattened into plain rows keyed by field name (Prometheus series are disambiguated by their labels), capped at `GRAFANA_MAX_ROWS`.

## Commands

| Command | What it does |
|---------|--------------|
| `grafana-mcp-server` (no args) | Run the MCP server on stdio — this is what the MCP client launches |
| `grafana-mcp-server login` | Open your default browser (dedicated profile), sign in to Grafana as usual, store the session for the server |
| `grafana-mcp-server login --browser X` | Force `chrome`, `msedge`, `arc`, `brave`, `vivaldi`, `chromium`, or a path to a Chromium-based binary |
| `grafana-mcp-server login --paste` | Store a `grafana_session` value read from stdin instead of opening a browser |
| `grafana-mcp-server logout` | Delete the stored session |

`npm run login` / `npm run logout` are shortcuts for the same thing from a checkout.

## Configuration

All configuration is via environment variables. With `login`, only `GRAFANA_URL` is needed.

| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `GRAFANA_URL` | **Yes** | | Grafana base URL |
| `GRAFANA_TOKEN` | No | | Service-account token (`glsa_…`) or legacy API key → Bearer. Overrides the stored session. |
| `GRAFANA_USERNAME` / `GRAFANA_PASSWORD` | No | | Basic auth |
| `GRAFANA_SESSION` | No | | A `grafana_session` cookie value (rotations can't be persisted from env — prefer `login`) |
| `GRAFANA_CREDENTIAL_FILE` | No | | File holding any one of the above; shape inferred, re-read per request, rotated sessions written back |
| `GRAFANA_SESSION_DIR` | No | `~/.grafana-mcp` | Where `login` keeps sessions (`sessions/<host>.json`) and its browser profile |
| `GRAFANA_LOGIN_TIMEOUT` | No | `300000` | How long `login` waits for you to finish signing in (ms) |
| `GRAFANA_CF_ACCESS` | No | `auto` | `off` to skip Cloudflare Access entirely |
| `CF_ACCESS_TOKEN` | No | *auto* | Cloudflare Access JWT fallback |
| `ALLOWED_DATASOURCES` | No | *(all)* | Comma-separated allowlist of datasource uids or names |
| `GRAFANA_MAX_ROWS` | No | `500` | Rows returned per frame |
| `GRAFANA_REQUEST_TIMEOUT` | No | `60000` | Request timeout in milliseconds |

Credential precedence: `GRAFANA_TOKEN` → `GRAFANA_USERNAME`+`PASSWORD` → `GRAFANA_SESSION` → `GRAFANA_CREDENTIAL_FILE` → the session stored by `login` → `~/.grafana-mcp-token` if it exists.

### Datasource allowlist

```json
"ALLOWED_DATASOURCES": "OsirT4Bnz,Prometheus"
```

`list_datasources` hides everything else and the query tools refuse it.

## Authentication

### Grafana: sign in with your own account (`login`)

`grafana-mcp-server login` gets you signed in and hands the session to the server. It drives **Chrome or Edge** — whichever is installed, used only as the sign-in window regardless of your default browser — through [Playwright](https://playwright.dev) in a dedicated profile under `~/.grafana-mcp/browser`, opens `GRAFANA_URL/login`, and waits while **you** sign in — Google, GitHub, SAML, LDAP, or a plain password; the tool never sees or types your credentials. When Grafana sets its `grafana_session` cookie the tool:

1. copies the session (and the `CF_Authorization` cookie, if Cloudflare Access is in front) into `~/.grafana-mcp/sessions/<host>.json` (0600),
2. **deletes Grafana's cookies from that browser profile**, so the server is the session's only holder,
3. calls `/api/user` through the server's own code path and prints who you are.

Playwright can drive only Chrome and Edge, and Arc can't be automated at all — so the sign-in window is always Chrome/Edge, never your default browser (which is left untouched). **Only if neither Chrome nor Edge is installed** does `login` fall back to opening Grafana in your default browser and walking you through copying the `grafana_session` cookie from DevTools once; that path also asks you to sign out of the tab afterwards so it stops competing for session rotations.

From then on the **server keeps the session alive itself**: Grafana answers a stale session with `401 session.token.rotate`, the server calls `POST /api/user/auth-tokens/rotate`, persists the new cookie atomically, and retries. The session therefore lasts for Grafana's login lifetime (30 days by default, 7 days idle) — re-run `login` when `health` says `failingLayer: "grafana"`.

Why this is the recommended path: it needs **no Grafana admin**. Everyone who can open Grafana in a browser can use the server, with exactly their own permissions, and revoking access is the same as for any user.

#### Why a pasted cookie normally dies in minutes

Grafana ≥ 10 rotates a session token every few minutes, and **only one client gets the new one**. If you copy `grafana_session` from DevTools while the tab stays open, the browser rotates it first and the copy is dead within minutes — that is the classic "the cookie stopped working" experience. `login` avoids it by taking the session out of the browser; `login --paste` works too as long as you then sign out (or clear the cookie) in the tab you copied from, because the server persists its own rotations.

### Grafana: other credentials

| Credential | Notes |
|-----------|-------|
| **Service-account token** (`glsa_…`) | Never rotates. Needs a Grafana admin to create it (*Administration → Service accounts*, role **Viewer**) — `scripts/mint-token.sh <admin-session>` automates that. Set `GRAFANA_TOKEN` or drop it in `GRAFANA_CREDENTIAL_FILE`. |
| Basic auth | Only if the login form is enabled — instances on Google/GitHub OAuth usually have no password to give. |
| `GRAFANA_SESSION` env | A session value straight from env. Works, but rotations cannot be written back, so it dies with the first rotation after a restart. Prefer `login`. |

`scripts/verify.sh` checks both layers and reports what the credential can see — without ever printing it.

### Cloudflare Access (optional)

If Grafana sits behind [Cloudflare Zero Trust](https://developers.cloudflare.com/cloudflare-one/), every request also needs a `cf-access-token` header or the edge redirects to its login page before Grafana sees the call. The server handles that automatically:

1. Read the cached JWT from `~/.cloudflared/<hostname>-<audience>-token` (written by `cloudflared access login`)
2. If missing or expired, run `cloudflared access token --app=<GRAFANA_URL>` (non-interactive)
3. Use the `CF_Authorization` cookie captured by `login` (if not expired)
4. Fall back to `CF_ACCESS_TOKEN`
5. On a 302 from the edge, drop the cached token and retry **once** with a fresh one

The token is resolved lazily, per request — never once at startup — so an expiring token heals itself instead of failing every call until the client restarts. Installing `cloudflared` is optional but recommended: its token refreshes itself for as long as your Cloudflare login lasts, whereas the cookie captured by `login` expires with Cloudflare's session policy (typically 24h) and then needs `login` again.

```bash
brew install cloudflared            # macOS; see Cloudflare's docs for Linux
cloudflared access login https://grafana.example.com
```

**Cloudflare does not log you into Grafana.** The CF JWT proves you may reach the host; Grafana then wants its own credential. A **302 / HTML** response is Cloudflare; a **JSON 401** is Grafana. `health` reports which.

Not behind Cloudflare? Set `GRAFANA_CF_ACCESS=off`, or just don't install `cloudflared` — the server skips the layer when no token can be found.

## Project Structure

```
src/
  main.ts                        # Entry point — `login` / `logout` commands, or the stdio MCP server
  login.ts                       # Browser sign-in (Playwright over your default Chromium-based browser) → session store
  default-browser.ts             # Detect the default browser (macOS LaunchServices / xdg) and whether it can be driven
  session-store.ts               # ~/.grafana-mcp/sessions/<host>.json, 0600, atomic writes
  grafana-client.ts              # Grafana HTTP client: two auth layers, CF retry, session rotation, datasource cache
  auth.ts                        # Grafana credential resolution (token / basic / session / file / login store)
  cf-token.ts                    # Cloudflare Access token: cache → cloudflared → login cookie → env, lazy + retry
  query-model.ts                 # Per-plugin /api/ds/query bodies (ClickHouse, SQL, PromQL/LogQL)
  allowed-datasources.ts         # Datasource allowlist
  utils/
    frames.ts                    # Data frames → rows
    format-response.ts           # Truncation, error results
  tools/
    list-datasources.tool.ts     # list_datasources
    query-sql.tool.ts            # query_sql
    query-metrics.tool.ts        # query_metrics
    search-dashboards.tool.ts    # search_dashboards
    get-dashboard.tool.ts        # get_dashboard
    health.tool.ts               # health
scripts/
  verify.sh                      # Check both auth layers; list what the credential can see
  mint-token.sh                  # Turn a fresh admin browser session into a service-account token
```

## License

MIT

TDQS

A4.2/5.0

Scored across 6 tools

Disambiguation5/5

Each tool targets a clearly distinct action: listing datasources, querying SQL, querying metrics/logs, searching dashboards, fetching dashboards, and checking health. The only similar pair, query_sql and query_metrics, is explicitly disambiguated by datasource type in list_datasources.

Naming Consistency4/5

All names use lowercase snake_case and mostly follow a verb_noun pattern: list_datasources, query_sql, query_metrics, search_dashboards, get_dashboard. The single outlier is health, which is a noun rather than a verb_noun name, but it is short and idiomatic enough not to cause confusion.

Tool Count5/5

Six tools is a well-scoped size for a Grafana MCP server focused on querying data and reusing existing dashboards. Each tool has a distinct role, and there is no sense of bloat or an overly thin surface.

Completeness5/5

The set covers the full discovery-to-query workflow: identify datasources, run the appropriate query type, search for existing dashboards, extract panel queries, and diagnose auth/connectivity failures. For a read-oriented Grafana data exploration server, there are no obvious dead ends or missing critical operations.

Maintenance

ActivityMaintained
ResponsivenessNo issues