grafana-editor
Provides the scaffolding for editing Grafana, currently offering authentication via Microsoft Entra ID SSO and a whoami tool; Grafana-specific editing tools are not yet implemented.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@grafana-editorWhat email am I signed in as?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
grafana-editor
An MCP server for editing Grafana, authenticated via Microsoft Entra ID SSO.
It exposes tools for exploring and querying one Grafana instance's datasources, and for building dashboards from what is there. A client can find out what data actually exists, confirm a query returns it, then create a dashboard and hand the user a link to look at.
Callers are authenticated as themselves against Entra ID; calls to Grafana are then made with a single Grafana service account token, so Grafana sees one identity rather than the signed-in user. Only reads are exposed today, so that distinction does not yet affect what anyone can do.
It uses:
uvfor project and dependency managementmcp/MCPServerfor the MCP protocol, using its resource-server-only OAuth supportstarletteas the ASGI applicationhypercornas the HTTP serverpyjwtto validate access tokens against their issuer's JWKSpytestfor tests
How authentication works
This server never talks to Entra to log anyone in. Instead:
An MCP client (e.g. Claude) fetches
{resource-server-url}/.well-known/oauth-protected-resourcefrom this server, which names Entra as the authorization server to use.The client runs the OAuth 2.1 authorization code flow (with PKCE) directly against Entra, in the user's browser.
Entra issues an access token scoped to this server's App ID URI, which the client sends as a
Bearertoken on every MCP request.This server validates that token's signature, issuer, audience and expiry against a configured
[[role]], and reads the caller's email out of it.
Those two halves are configured separately, because they are separate things:
resource-server-url,originandscopesare what step 1 publishes: where this server is reachable, which authorization server to authenticate against, and which scope to ask it for.each
[[role]]says which tokens step 4 accepts, as anissuer/audiencepair. Signing keys are found by readingjwks_uriout of{issuer}/.well-known/openid-configuration, so nothing about the provider is hardcoded; a role may setjwks-uriitself for an issuer that publishes no discovery document. A token is accepted if it matches any one role, so a second issuer just means a second[[role]].
An issuer is not the same thing as the authorization server clients talk
to, and for Entra it usually isn't: an app registration whose manifest
leaves requestedAccessTokenVersion at its default issues v1 tokens, whose
iss is https://sts.windows.net/{tenant}/, even though the client
authenticated via the v2 endpoints that origin names. Read the iss
claim of a token your tenant actually issues rather than assuming — a
rejected token is logged with both the expected and the received value.
That means setting this up requires an app registration in Entra ID:
Create an app registration. Note its Application (client) ID and your Directory (tenant) ID.
Under "Expose an API", accept the default Application ID URI (
api://<client-id>) and add a scope (e.g.mcp.access).Under "Token configuration", add
emailas an optional claim on the access token — thepreferred_usernameclaim Entra includes by default is usually an email address too, but isn't guaranteed to be one.Register the MCP client as an authorized client for that scope (Entra's support for OAuth dynamic client registration is limited, so which clients this covers depends on how your MCP client obtains credentials — check its docs for connecting to an Entra-protected MCP server).
Fill in this server's own public URL, the tenant's v2 endpoint as origin,
and the role's issuer and audience in config.toml.example. List the
scope from step 2 in scopes, qualified with the Application ID URI
(api://<client-id>/mcp.access) — that is the form Entra resolves a scope
against. It is both advertised as scopes_supported in this server's OAuth
Protected Resource Metadata and required on every request. Advertising it is
what stops clients falling back to requesting only generic OIDC scopes
(openid profile email offline_access), none of which belong to this
resource — which Entra rejects with AADSTS9010010.
Staying signed in past the first hour
One of those generic scopes is load-bearing, though: offline_access is what
makes Entra return a refresh token alongside the access token. A client
takes the scope of its authorization request from scopes_supported (RFC
9728 metadata is the highest-priority source it has) and asks for nothing
else, so advertising only mcp.access gets a bare access token. Entra's
default access-token lifetime is 60–75 minutes, after which the client has
nothing to refresh with and the connection can only be restored by
authorizing again from scratch — which looks like being logged out after an
hour and having to disconnect and reconnect the connector.
So offline_access is advertised in scopes_supported on top of whatever
scopes lists, and is not added to the scopes required per request: it
belongs to the authorization server rather than to this resource, so it never
appears in the scp claim of a token minted for our audience, and requiring
it would reject every request with insufficient_scope. Those two lists
being different is why this server publishes the metadata document itself
rather than letting the SDK derive it from AuthSettings.required_scopes,
which does both jobs with one list.
Set offline-access = false to stop advertising it, for an authorization
server that rejects the scope outright. Expect the authorization prompt to
ask for consent explicitly the first time, as OIDC requires for
offline_access.
A note on custom Application ID URIs and trailing slashes
If you customise the Application ID URI to an https:// URL under your own
domain (rather than the default api://<client-id>), set the role's
audience to exactly that value, and keep resource-server-url and the
prefix of each entry in scopes the same string too. Entra refuses to register an Application ID URI that ends in a
slash, so the whole set should be slash-free.
Take care not to let a trailing slash creep back in: pydantic's AnyHttpUrl
normalises https://host to https://host/, so AuthSettings is handed
plain strings (it sets url_preserve_empty_path, keeping the slash-free
form) rather than URLs we built ourselves. A mismatch here shows up as an
aud claim that doesn't match audience — the rejection log names both
values.
Related MCP server: Grafana FastMCP Server
How it talks to Grafana
Every Grafana call carries a bearer token read from the file named by
grafana.service-account-token-path. Naming the token by path rather than by
value keeps it out of the config file, which is a ConfigMap in the deployed
setup, and lets it come from a mounted Kubernetes secret instead. The file is
re-read whenever its size or mtime changes, so rotating the secret does not
need a restart.
Create the token in Grafana under Administration → Users and access →
Service accounts. It needs the Viewer role, which carries
datasources:read and datasources:query, plus Edit permission on the
sandbox folder — granted on the folder itself under its Permissions tab,
rather than by giving the service account the Editor role globally.
Datasource queries go through Grafana's datasource proxy
(/api/datasources/proxy/uid/...), so Grafana's own datasource
authentication and access control still apply and this server never needs
credentials for Mimir or Loki themselves.
Prometheus-shaped datasources (prometheus, which is also how Grafana reports
a Mimir or Thanos backend, and grafana-amazonprometheus-datasource) and
loki are queryable. list_datasources reports queryable: false for
anything else, and a query against one names the type it got and the types it
needed.
Run it
cp config.toml.example config.toml
# edit config.toml with your tenant details
uv run grafana-editor --config config.tomlWithout Entra configured, for local development only:
uv run grafana-editor --config config.toml --disable-authWith --disable-auth, the whoami tool has no signed-in user to report and
returns an error explaining that — it does not fabricate an email address.
Then connect an MCP client or the inspector to:
http://127.0.0.1:8000/mcpExposed MCP data
Tools:
whoami— returns{"email": "..."}for the signed-in user.list_datasources— the datasources Grafana has, with their uids, types, and whether this server can query them.list_metrics— metric names on a Prometheus-shaped datasource.describe_metrics— the type, unit and help text a metric was exported with, which is what decides whether a panel needsrate()and how it should be formatted.list_labels— label names on a Prometheus or Loki datasource.list_label_values— the values one label takes; this is what fills in a dashboard template variable's options.query_instant— evaluate PromQL or LogQL at a single instant.query_range— evaluate PromQL or LogQL over a window, which is the shape a time series panel draws.create_dashboard— create a dashboard in the sandbox folder, returning the URL to open it at.update_dashboard— replace a sandbox dashboard's contents, for iterating on one rather than creating another.get_dashboard— the stored dashboard JSON, its URL and version, and whether this server may write to it.list_dashboards— what is in the sandbox folder, by default only the caller's own.
Three conventions run through all of them, so that a caller does not have to carry Grafana's quirks:
A datasource is named by uid or display name, whichever the caller happens to be holding.
Times are given as an RFC 3339 timestamp, unix seconds, or a relative expression (
now,now-15m,now-6h). Omitting them queries the last hour. The server sends RFC 3339 onwards, which sidesteps Prometheus reading a bare number as unix seconds while Loki reads it as nanoseconds.A range query with no
stepgets one that yields a couple of hundred points over the window, snapped to a value a human would have picked, and the reply reports which step was used.
Large results are truncated rather than returned in full, and a truncated
result carries a truncated field saying what was dropped and how to ask for
less.
Writing dashboards
Two things are true of every dashboard this server writes, and neither is up to the caller.
It goes in the sandbox folder. grafana.sandbox-folder names it
(default Sandbox), and nothing else is written to. update_dashboard
checks the folder a dashboard is actually in before replacing it, so a uid
from elsewhere is refused here rather than at Grafana's permission check.
Its title is prefixed with the caller's name, taken from the part of
their SSO email address before the @: a create_dashboard for
Server Temperature by noa.resare@portswigger.net is saved as
noa.resare: Server Temperature. The sandbox is shared, so the prefix is
what makes it obvious whose a dashboard is. The rest of the title is passed
through exactly as given — capitalisation included, since title-casing would
turn CPU usage into Cpu usage — and a title that already carries the
prefix is not prefixed twice. The same name also goes on as an
author:<name> tag, which is what lets list_dashboards filter by author
without matching on titles.
Beyond that the caller supplies the Grafana dashboard object itself, so
anything Grafana's dashboard JSON supports is reachable. The server sets
title, uid, id and the folder, fills in schemaVersion, time,
timezone and editable when they are absent, and passes everything else
through.
update_dashboard replaces a dashboard rather than merging into it, and
sends the version it just read, so a save that would discard an edit someone
made in the Grafana UI meanwhile fails with Grafana's version conflict
instead of winning silently. To change part of a dashboard, read it with
get_dashboard first.
Test it
uv run pytestBuild a container image
docker build -t grafana-editor .The image expects its config at /config/grafana-editor.toml by default.
This server cannot be deployed
Maintenance
Related MCP Connectors
Prove end users to agents and apps: login-links, OIDC clients, and API keys over remote MCP
Governed MCP gateway: one endpoint for your tools, with credential custody and audit log.
Remote MCP for Copilot CLI switch gate MCP, structured receipts, audit logs, and reviewer-ready evid
- SkycloakOAuthio.skycloak
Managed Keycloak from any MCP client: clusters, realms, apps, SSO, users, domains, audit events.
Related MCP Servers
- -licenseNot gradedqualityNot gradedmaintenanceEnables remote MCP client connections with WorkOS AuthKit authentication, supporting organization-centric user management and permission-based tool access control.-
- AlicenseNot gradedqualityDmaintenanceEnables MCP-compatible agents to interact with Grafana instances for searching, creating, and updating dashboards, exploring logs via Loki, querying datasources, managing alerts, incidents, and on-call shifts, and accessing observability data.8Apache 2.0
- FlicenseNot gradedqualityCmaintenanceEnables querying ShareFile OpenSearch production logs via MCP with automatic EntraID OAuth authentication.-
- AlicenseNot gradedqualityCmaintenanceEnables MCP clients to fetch the signed-in Microsoft Entra user's profile from Microsoft Graph via the GET /me endpoint using the provided bearer token.Apache 2.0