Key Takeaway
The Grafana Cloud MCP server at mcp.grafana.com/mcp uses OAuth 2.1 with three consent levels: Read, Query and Write. Query adds raw SQL that can modify data, so clearing only Write does not make a connection read-only. Access tokens last one hour and sessions 30 days, so unattended automation needs a registered OAuth client.
The question that made me read the Grafana Cloud MCP documentation line by line was a small one: if I point Claude Code at production telemetry, what is the worst thing it can do with the token it ends up holding? The consent page offers three access levels, and the obvious reading of them turns out to be slightly wrong.
This post is a close reading of Grafana's own documentation for the hosted server, its OAuth client registration guide and the MCP authorization specification, set against the Claude Code documentation for connecting and restricting servers. I have not run it against a live stack for this write-up, so every behaviour below is attributed to the page that states it, and where I am inferring I say so.
Three scopes, and what each one adds
The server is a hosted endpoint at https://mcp.grafana.com/mcp, and it is user-scoped: the agent gets only the permissions your Grafana RBAC grants you. On top of that, the consent page asks how much of your access you want to hand to this particular client. The documentation defines three levels, each including the one below it.
| Consent level | What the docs say it adds | Representative tools |
|---|---|---|
| Read | View dashboards, alerts, incidents, metrics, logs and traces | search_dashboards, query_prometheus, query_loki_logs, list_incidents, get_incident |
| Query | Also run raw SQL. Queries run as written and can modify data | The docs do not name which tools; the SQL-shaped ones in the list are the ClickHouse tools |
| Write | Also create and modify dashboards, alerts, incidents and Assistant investigations. Includes read and query access | update_dashboard, create_folder, create_annotation, create_incident, create_investigation, ask_assistant |
| Read-only | Clear both Write and Query on the consent page | Read tools only; tools marked Write need the grafana:write scope |
Two role details sit behind the consent page. To connect at all you need the Assistant Cloud MCP User role or the grafana-assistant-app.cloud-mcp:access permission, which Editor and higher hold by default. Write access needs more: Organization Admins have it by default, and everyone else needs the Assistant Admin role.
Query is not read, and clearing only Write proves it
The sentence that matters most in the scope documentation is the one about read-only: to get a read-only connection you clear both Write and Query. Clearing only Write still allows raw SQL if Query is selected. The ordering is cumulative, but the boxes on the consent page are separate, so it is possible to land on a connection that looks cautious and is not.
The reason is in the Query definition itself: raw SQL runs as written and can modify data. A person at a SQL console knows that. An agent composing a statement from a half-read schema is a different risk, and a scope named Query does not announce it. When I decide what to grant a coding agent, I treat Query as the first level that can change state, not the last level that cannot.
Read-only means both Write and Query cleared. If you tick only Query because you want the agent to explore a ClickHouse data source, you have granted a path that can modify data, and the Write box being empty does not change that.
Connecting Claude Code, one entry per stack
Claude Code adds a remote server with claude mcp add and the http transport. Grafana's page shows the one-line form, and a form that passes your stack as an X-Grafana-URL header so the browser goes straight to the consent page. For more than one stack, the page is explicit: use one entry per stack with the stack in the endpoint path, and do not reuse the same endpoint with different headers, because Cursor and Claude Code treat those as duplicates.
# Right: one entry per stack, the stack in the endpoint path.
# --scope project writes .mcp.json so the team shares the endpoint;
# each person still authorises with their own Grafana identity.
claude mcp add grafana-prod https://mcp.grafana.com/mcp/<prod-stack>.grafana.net \
--transport http --scope project
claude mcp add grafana-staging https://mcp.grafana.com/mcp/<staging-stack>.grafana.net \
--transport http --scope project
# Wrong: the same endpoint twice, told apart only by a header.
# Cursor and Claude Code treat these as duplicates of one server.
claude mcp add grafana-a https://mcp.grafana.com/mcp --transport http \
--header "X-Grafana-URL: https://<prod-stack>.grafana.net"
claude mcp add grafana-b https://mcp.grafana.com/mcp --transport http \
--header "X-Grafana-URL: https://<staging-stack>.grafana.net"
# Authenticate. --no-browser prints the URL, handy over SSH.
claude mcp login grafana-prod --no-browser
# Inside a session, /mcp shows status and lets you re-authenticate.
claude mcp listAuthentication then happens in the browser. Inside Claude Code you run /mcp and complete the login, or from a shell you use claude mcp login with the server name; the --no-browser flag prints a URL and asks you to paste the redirect back, which is the form you want over SSH. Where the entry lives matters too: local scope is the default and is private to you, while project scope writes a .mcp.json you can commit, so a team shares the endpoint but every person still authorises with their own Grafana identity.
Name the servers by environment, such as grafana-prod and grafana-staging. The server name becomes part of every tool name Claude Code exposes, which is what permission rules match against in section six, so a name you can read in a deny rule is worth choosing now.
Cursor, VS Code and the other clients
Every client in Grafana's page uses the same endpoint and the Streamable HTTP transport; they differ only in the config file and in whether the transport key is called type. Cursor and Windsurf take a url and headers, while Claude Desktop and VS Code add an explicit type of http, and VS Code nests everything under servers instead of mcpServers.
// .cursor/mcp.json (Windsurf is the same shape)
{
"mcpServers": {
"grafana": {
"url": "https://mcp.grafana.com/mcp",
"headers": { "X-Grafana-URL": "https://<your-stack>.grafana.net" }
}
}
}
// .vscode/mcp.json (note "servers", and the explicit type)
{
"servers": {
"grafana": {
"type": "http",
"url": "https://mcp.grafana.com/mcp",
"headers": { "X-Grafana-URL": "https://<your-stack>.grafana.net" }
}
}
}Streamable HTTP is the only transport. SSE is not supported by the server, and Claude Code separately marks its own sse transport as deprecated, so there is no fallback to reach for if a client only speaks SSE. The server is also Grafana Cloud only; a self-hosted Grafana needs the local open source server instead, which section seven compares.
One hour, thirty days, and what that means for automation
The documented lifetimes are simple. The access token is valid for one hour and is refreshed automatically. The session lasts 30 days; after that you must log in and re-authorise. A person can revoke a connection under Settings, Connectors, MCP clients, Your connections. For an interactive coding session that is invisible, and it is also what the MCP authorization specification asks for: authorization servers should issue short-lived access tokens, and for public clients they must rotate refresh tokens.
For unattended automation the interesting line is the one Grafana puts on its client registration page: clients that do not support Dynamic Client Registration or Client ID Metadata Documents need a manually registered client. An Assistant Admin registers it under Assistant, Settings, Connectors, MCP clients, picks the maximum access it may ever be granted, and copies the client ID and secret, which Grafana shows once. The client must support Streamable HTTP and the authorization code flow with PKCE using S256.
# After an Assistant Admin registers a client (Assistant > Settings >
# Connectors > MCP clients > OAuth clients > Register client), pass its
# credentials to Claude Code. --client-secret prompts with masked input;
# in CI set MCP_CLIENT_SECRET instead.
# The redirect URL registered in Grafana must match exactly:
# http://localhost:8080/callback (HTTP is allowed only for loopback)
claude mcp add --transport http \
--client-id your-client-id --client-secret --callback-port 8080 \
grafana-ci https://mcp.grafana.com/mcp/<stack>.grafana.netThat registration identifies the application, not a user. Each person still signs in and authorises their own connection under their own Grafana permissions, and registering a client grants no data access by itself. The page does not address service accounts or headless use, so I would not promise a team a bot account on this path. What it does give you is a ceiling: the allowed-access setting caps what any user can grant, and editing it affects only new authorisations.
Layer a client-side rule on top of the consent page
Consent is a server-side control and a deny rule is a client-side one; I want both. Claude Code names MCP tools mcp__server__tool and lets permission rules match that exact name. A deny rule removes the tool from what the model can call, whatever the server would have allowed. Note that rules with parentheses are skipped for mcp__ tools, so use plain names.
// .claude/settings.json
// The server segment must match the name you gave "claude mcp add".
// Use plain tool names: mcp__ rules containing parentheses are skipped.
{
"permissions": {
"allow": [
"mcp__grafana-prod__search_dashboards",
"mcp__grafana-prod__list_datasources",
"mcp__grafana-prod__query_prometheus",
"mcp__grafana-prod__query_loki_logs"
],
"deny": [
"mcp__grafana-prod__update_dashboard",
"mcp__grafana-prod__create_folder",
"mcp__grafana-prod__create_annotation",
"mcp__grafana-prod__update_annotation",
"mcp__grafana-prod__create_incident",
"mcp__grafana-prod__add_activity_to_incident",
"mcp__grafana-prod__create_investigation",
"mcp__grafana-prod__ask_assistant"
]
}
}One catch comes from the tool list rather than from any single sentence. alerting_manage_rules is marked Read and Write in a single tool, so a tool-level deny would remove alert reading along with alert editing. That is my inference from the table, not a documented behaviour, but it is why I do not rely on deny rules alone: the consent scope is the control that separates read from write inside one tool, and the deny list covers the clearly write-only ones.
Cloud MCP or the open source server
Grafana ships two servers, and the choice is mostly about who holds the credential. The hosted one is OAuth and user-scoped; the open source mcp-grafana runs where you run it and authenticates with a service account token passed as GRAFANA_SERVICE_ACCOUNT_TOKEN, alongside GRAFANA_URL.
| Concern | Cloud MCP server | Open source mcp-grafana |
|---|---|---|
| Who runs it | Grafana, fully managed; no local process to host or update | You, for example with uvx mcp-grafana |
| Credential | OAuth 2.1, one browser consent per user | A service account token in an environment variable |
| Whose permissions | The signed-in user, further capped by the consent level | Whatever you grant that service account |
| Works with | Grafana Cloud stacks only, not self-hosted | Self-hosted Grafana; the docs ask for Grafana 9.0 or later for full functionality |
| Usage accounting | MCP users count as active Assistant users; read-only use does not consume Assistant tokens | The page I read does not say |
The trade is audit versus convenience. With the hosted server every query runs as a named human, which is the property I want from a tool an AI is steering. A shared service account token is easier to script and harder to attribute. I would take the first for exploration and the second only with a deliberately narrow account.
What to do on Monday
None of this needs new infrastructure. It needs a few deliberate choices made before the first connection, because the consent page defaults are not yours.
- Confirm the stack is Grafana Cloud and that you hold the Assistant Cloud MCP User role or the cloud-mcp:access permission.
- Add one server per stack in project scope, named by environment, with the stack in the endpoint path.
- Authorise production with both Write and Query cleared. Raise to Query only for a named task, on staging first.
- Commit a permissions block that denies the write-only tools by their mcp__ names.
- If you want CI to use it, ask an Assistant Admin for a registered client capped at Read only, and plan for the 30-day re-authorisation.
Then ask it one read question you already know the answer to, such as the current error rate of a service, and compare. If the agent's number matches your dashboard you have proven the path end to end before you trust it with an unfamiliar one.
A scope name is a promise about the consent page, not about the agent, and the useful rule is to grant the least level that answers the question: Read for exploration, Query only for a named task, Write almost never, with a deny rule and a short session behind it.
Sources and further reading
- Grafana Cloud docs: Cloud MCP server
- Grafana Cloud docs: register an OAuth client for MCP
- Grafana what's new: Cloud MCP server, connect your agents with a single URL
- Grafana Cloud docs: open source Grafana MCP server
- Model Context Protocol specification: authorization
- Claude Code docs: connect to tools via MCP
- Claude Code docs: configure permissions

