The Model Context Protocol lets an AI client call tools on your behalf. Grafana maintains its own MCP server, mcp-grafana, which exposes your Grafana instance as a set of callable tools: search dashboards, run PromQL and LogQL queries, read alert rules, list data sources, manage incidents.
The practical result is that you can ask an assistant "what fired last night on the api dashboard" and it goes and looks. Setting it up takes about ten minutes. Deciding what token to give it takes longer, which is why the security section at the end runs as long as it does.
What the Grafana MCP server exposes
The mcp-grafana repository ships well over a hundred tools grouped into categories. The ones enabled out of the box cover most of what you would want:
| Category | What it can do |
|---|---|
| Search and Dashboard | Find dashboards by title, fetch one by UID, get a compact summary or a single JSONPath property, read every panel's query and data source, create or patch dashboards |
| Datasources | List configured data sources and fetch details for one by UID or name |
| Prometheus | Run instant and range PromQL queries, list metric names, label names and label values, read metric metadata, compute histogram percentiles |
| Loki | Run log and metric LogQL queries, read label names and values, retrieve detected log patterns |
| Alerting | List alert rules and their firing state, create and update rules, read notification policies and contact points |
| Incident, OnCall and Sift | Search and create incidents, read on-call schedules and shifts, run Sift investigations for error patterns and slow requests |
| Annotations, Snapshots, Navigation | Read and write annotations, create snapshots, generate deeplinks into the Grafana UI |
A second set ships disabled by default and has to be opted into: admin, agento11y, assistant, athena, clickhouse, cloudwatch, elasticsearch, examples, graphite, quickwit, runpanelquery and snowflake. The specialised data source ones are off because most instances do not have those plugins. admin is off because team and user management is not something you want an assistant reaching for by accident.
The README notes Grafana 9.0 or later is required for full functionality, since some data source endpoints do not exist before that. Starting from nothing, a package install of Grafana on Ubuntu puts you on a current release by default. Anything on 12.x or 13.x is well clear of that line, so the version question rarely comes up.
Create the service account and token
The server authenticates to Grafana with a service account token, so make that first. Under Administration you want Users and access, and the Service accounts page inside it. Create one called mcp-readonly and set the role.
Naming is worth the thirty seconds. Six months from now somebody doing an audit will find a token called test and have no way to tell what breaks if they revoke it, except by revoking it and waiting to see who complains. That is the whole digression. Back to the role.
Set the role to Viewer. The repository is candid that assigning the built-in Editor role is the quicker path and says to use it "only when convenience is more important than strict least-privilege access". Viewer covers every read tool, which is every tool most people use an assistant for. Add write permissions later if you find yourself needing them.
Now generate a token on that account and put an expiry date on it. Grafana displays the value one time only. It begins glsa_, and every request the MCP server makes carries it in an Authorization: Bearer header, which is the same mechanism the Grafana API and service accounts guide describes for scripts and CI jobs. Paste it somewhere safe before you close the dialog.
Expiry can be enforced from /etc/grafana/grafana.ini:
[service_accounts]
token_expiration_day_limit = 30
That key caps how far out an expiry date can be set when somebody creates a token. Its value in conf/defaults.ini is empty, no cap, which is why most people's tokens never expire.
Install mcp-grafana
Five install paths, listed in the official Grafana MCP documentation as well as the repository. Which one you want depends mostly on where the client runs.
uvx
The repository calls this the quick start and it needs nothing installed except uv. The client downloads and runs the server on demand:
uvx mcp-grafana
Docker
docker pull grafana/mcp-grafana
docker run --rm -i \
-e GRAFANA_URL=http://localhost:3000 \
-e GRAFANA_SERVICE_ACCOUNT_TOKEN=<your service account token> \
grafana/mcp-grafana -t stdio
Read that -t stdio carefully, because the image's entrypoint defaults to SSE mode. Wiring it to a desktop client over stdio means overriding that default and passing -i to keep stdin open. Leave either out and you get a container that starts, binds a port nobody is talking to and a client that reports the server as unavailable. Why the image defaults to one transport while the binary defaults to another, I do not know.
Binary
Grab a build from the releases page and drop it somewhere on your $PATH. That's the route I'd take on a server. A single static binary under systemd has fewer moving parts when the thing it talks to is already on the same host.
go install
GOBIN="$HOME/go/bin" go install github.com/grafana/mcp-grafana/cmd/mcp-grafana@latest
Helm
helm repo add grafana https://grafana.github.io/helm-charts
helm install --set grafana.apiKey=<Grafana_ApiKey> --set grafana.url=<GrafanaUrl> my-release grafana/grafana-mcp
stdio, SSE and streamable HTTP transports
The -t flag (long form --transport) takes stdio, sse or streamable-http, and the binary defaults to stdio.
stdio is a local pipe. The client launches the server as a child process and they talk over standard input and output. There is no port and no listener, so there is nothing to authenticate because nothing else can reach it. For a desktop client on the same machine, use it.
sse and streamable-http both bind a listener. Relevant flags:
| Flag | Default | Purpose |
|---|---|---|
--address | localhost:8000 | Host and port for the HTTP transports |
--endpoint-path | /mcp | Endpoint path for streamable HTTP |
--base-path | empty | Base path prefix for the server |
--grafana-timeout | 10s | Request timeout against Grafana, Go duration format |
--log-level | info | debug, info, warn or error |
--session-idle-timeout-minutes | 30 | Idle sessions are reaped. 0 disables reaping |
Defaults, gathered in one place. The transport is stdio. The address is localhost:8000, the endpoint path is /mcp and the base path is empty. The Grafana request timeout is 10s, the log level is info and idle sessions are reaped after 30 minutes.
Both HTTP transports expose GET /healthz, which returns 200 OK with a body of ok. That is what you point a load balancer or a container health check at. Stdio has no HTTP server so it has no health endpoint.
Client configuration
Almost every MCP client uses the same JSON shape: a top-level mcpServers object, one key per server, and under that key a command, an args array and an env object. Here is the concrete form for Claude Desktop, using the binary:
{
"mcpServers": {
"grafana": {
"command": "mcp-grafana",
"args": [],
"env": {
"GRAFANA_URL": "http://localhost:3000",
"GRAFANA_SERVICE_ACCOUNT_TOKEN": "<your service account token>"
}
}
}
}
Swap "command": "mcp-grafana" for "command": "uvx", "args": ["mcp-grafana"] if you went the uvx route, or point command at docker with the run arguments in args. Cursor, VS Code and the rest use the same three fields, sometimes under a different filename and occasionally with the block nested one level deeper. Client config formats move around, so treat the shape as the thing to remember and check your client's own docs for where the file lives.
With an HTTP transport the client config carries a URL, plus headers if you have enabled caller authentication. The environment variables:
| Variable | What it sets |
|---|---|
GRAFANA_URL | Your instance, for example http://localhost:3000 or https://myinstance.grafana.net |
GRAFANA_SERVICE_ACCOUNT_TOKEN | The token. Replaces the deprecated GRAFANA_API_KEY |
GRAFANA_SERVICE_ACCOUNT_TOKEN_FILE | Path to a file holding the token, re-read on every request so rotation needs no restart |
MCP_GRAFANA_SERVER_TOKEN | Bearer token callers must present. Env fallback for --server-auth-token |
GRAFANA_ORG_ID | Numeric org ID for multi-org instances |
Use the token file variable on anything long-running. The file is read fresh on every request, so a rotated secret is picked up with no restart and no gap, which starts to matter once the token has a thirty day expiry.
Tool categories and turning them off
--enabled-tools takes a comma-separated list of categories and replaces the default set, so use it to switch on something that ships disabled:
mcp-grafana --enabled-tools "search,dashboard,datasource,prometheus,loki,alerting"
Individual --disable-<category> flags subtract from whatever is enabled. --disable-oncall, --disable-navigation, --disable-snapshot and so on, one per category.
--disable-write turns off every mutating tool in one go: dashboard updates, folder creation, incident creation, alert rule and silence management, annotation writes, snapshot creation and the Sift tools that create investigations. Every read tool keeps working. Combine it with a Viewer service account and you have two independent things saying no.
There is a context window argument for trimming tools too. A hundred-plus tool definitions is a lot of tokens before the conversation even starts, and cutting the categories you do not use makes the assistant faster and more accurate at picking the right tool. If you only ever ask about dashboards and Prometheus, enable those two and leave the rest off.
Running it next to a self-hosted Grafana
If Grafana is already in Docker, the MCP server goes on the same user-defined network and reaches it by container name:
services:
mcp-grafana:
image: grafana/mcp-grafana
command: ["-t", "streamable-http", "--address", "127.0.0.1:8000", "--disable-write"]
environment:
GRAFANA_URL: "http://grafana:3000"
GRAFANA_SERVICE_ACCOUNT_TOKEN: "${GRAFANA_MCP_TOKEN}"
MCP_GRAFANA_SERVER_TOKEN: "${MCP_CALLER_TOKEN}"
ports:
- "127.0.0.1:8000:8000"
restart: unless-stopped
The URL is http://grafana:3000, because localhost inside a container is that container. The port mapping is written as 127.0.0.1:8000:8000 rather than 8000:8000 so Docker publishes it on loopback only, which matters because Docker's port publishing writes iptables rules that bypass ufw. The Grafana side of that file is in the Grafana Docker Compose stack, where the two services already share a network.
Add the health check while you are in there, since the endpoint exists and costs nothing:
curl -s http://127.0.0.1:8000/healthz
Grafana Cloud's hosted MCP endpoint
Grafana Cloud runs a hosted server, so there is nothing to install. It lives at https://mcp.grafana.com/mcp and authenticates with an OAuth 2.1 flow in the browser. The client block is a URL with an optional header naming your stack:
{
"mcpServers": {
"grafana": {
"url": "https://mcp.grafana.com/mcp",
"headers": {
"X-Grafana-URL": "https://<your-stack>.grafana.net"
}
}
}
}
The Cloud MCP documentation describes the header as optional but recommended, since it skips the URL entry step during authorization. Cloud MCP access is user-scoped through Grafana RBAC, so it inherits the permissions of whoever authorized it. That is a better security model than a shared service account, and it is the main reason to prefer it if Cloud is where your Grafana already lives.
Security: What a read-everything token exposes
A service account token that can read every dashboard is a bigger exposure than it sounds. Specifically:
Dashboards contain queries. Queries contain hostnames, internal service names, database names, Kubernetes namespaces, customer identifiers in label values and sometimes credentials in a URL somebody pasted into a panel description. A read-only token over the dashboard API is a fairly complete map of your infrastructure. Add datasources:query and it can also run arbitrary PromQL and LogQL, which means it can read log lines, and log lines contain whatever your applications log.
So, four things I would do on any deployment that outlives an afternoon.
Scope the service account to Viewer, and to specific resources where you can. RBAC scopes accept UIDs, so datasources:uid:prometheus-prod with datasources:query grants exactly one data source rather than all of them. It is more work than assigning Editor. It also decides how bad a day a leaked token turns into.
Keep the listener on loopback. The defaults help here. --allowed-hosts defaults to the loopback variants of --address and rejects anything else with a 403, which blocks DNS rebinding from a browser. --allowed-origins is empty by default, so any request carrying an Origin header is rejected outright. Both accept * to switch the check off, which is only sane behind a reverse proxy that rewrites Host. There is no reason for this server to be reachable from the internet at all.
Set a caller token if it binds anything other than loopback. --server-auth-token, or better MCP_GRAFANA_SERVER_TOKEN so the secret does not sit in the process arguments where ps can read it. Without one, the server starts anyway and logs a security error at error level, and the README says a future major release will make that a startup failure. Treat the warning as the failure it is going to become.
Run with --disable-write unless you have a specific reason not to. The tools that create incidents and rewrite dashboards belong in a workflow somebody designed on purpose. An assistant that can silence an alert rule is an assistant that can silence the alert rule you needed.
Rotation is the other half. Give the token a 30 day expiry, point GRAFANA_SERVICE_ACCOUNT_TOKEN_FILE at a file and replace the file on a schedule. GET /api/serviceaccounts/:id/tokens lists what a service account is carrying, expiry included, so an audit is a one-line script. Everything else on the same instance sits in the Grafana hardening checklist, which is a considerably longer list than this one.
One operational note to finish on. Turn on --debug the first time you connect a client and watch the HTTP request and response log go past. It shows you exactly which Grafana endpoints the assistant is hitting and with what, which is the fastest way to understand what a tool call really does, and it is the log you will want when a permission error comes back as a generic failure in the client UI. Then turn it off, because it prints request bodies.

