grafana-unified-mcp

grafana-unified-mcp

Enables managing multiple Grafana instances through a single MCP server, routing requests to the correct instance based on an instance argument.

Category
访问服务器

README

grafana-unified-mcp

One MCP server in front of many Grafana instances. Every tool from the standard Grafana MCP server, plus one extra argument — instance — that says which Grafana to run it against.

query_prometheus(instance="tenant-a", expr="up", datasourceUid="...")
search_dashboards(instance="tenant-b", query="login latency")

Why this exists

Upstream grafana/mcp-grafana binds GRAFANA_URL once, at process start. It reads X-Grafana-Service-Account-Token per request, but the URL is fixed — and the header that used to override it is now explicitly inert. From upstream validate_url.go:

Deprecated: X-Grafana-URL no longer configures the Grafana client. This middleware is retained temporarily to preserve malformed-header handling.

So one mcp-grafana process can only ever talk to one Grafana. Ten Grafanas means ten servers, ten entries in every client config, and ten sets of tools with identical names for the model to disambiguate.

This server fixes that by running one upstream child process per instance and routing each call to the right one based on the instance argument. Tools are discovered from the real binary at runtime, so you get whatever upstream exposes — currently 65 tools — with no per-tool code here and nothing to update when upstream adds more.

How it works

                                        ┌──────────────────────────────────┐
  Claude Code / routines /              │  grafana-unified-mcp             │
  cloud sessions                        │                                  │
        │                               │  ┌────────────────────────────┐  │
        │  streamable-HTTP              │  │ bearer auth                │  │
        │  Authorization: Bearer …      │  │  → Principal(instances,    │  │
        ├──────────────────────────────►│  │      read-only|read-write) │  │
        │                               │  └────────────┬───────────────┘  │
        │                               │               │                  │
        │                               │  ┌────────────▼───────────────┐  │
        │                               │  │ catalog: inject `instance`  │  │
        │                               │  │ filter by caller's grant   │  │
        │                               │  └────────────┬───────────────┘  │
        │                               │               │ route on         │
        │                               │               │ instance=…       │
        │                               │  ┌────────────▼───────────────┐  │
        │                               │  │ child pool (lazy, reaped)  │  │
        │                               │  └──┬──────────┬──────────┬───┘  │
        └───────────────────────────────┴─────┼──────────┼──────────┼──────┘
                                              │ stdio    │ stdio    │ stdio
                                        ┌─────▼────┐ ┌───▼──────┐ ┌▼─────────┐
                                        │mcp-grafana│ │mcp-grafana│ │mcp-grafana│
                                        │ tenant-a │ │ tenant-b │ │   …      │
                                        └─────┬────┘ └───┬──────┘ └┬─────────┘
                                              ▼          ▼         ▼
                                          tenant-a    tenant-b   …Grafana

Children start on first use, stay warm, get reaped when idle (--idle-timeout, default 15 min), and are respawned transparently if they die. An unreachable Grafana degrades only its own instance.

Install

Two pieces: the upstream binary, and this package.

# 1. the upstream mcp-grafana binary (needs Go 1.26+; GOTOOLCHAIN=auto fetches it)
deploy/install-mcp-grafana.sh /usr/local/bin

# 2. this server
python3 -m venv /opt/grafana-unified-mcp/.venv
/opt/grafana-unified-mcp/.venv/bin/pip install 'grafana-unified-mcp[aws] @ .'

If you already have the binary, point at it with MCP_GRAFANA_BINARY=/path/to/mcp-grafana or --mcp-grafana-binary.

Configure

Endpoints

Exactly the shape you'd expect — instance name to the upstream env vars:

{
  "tenant-a": {
    "GRAFANA_URL": "https://tenant-a.example.cloud/grafana",
    "GRAFANA_SERVICE_ACCOUNT_TOKEN": "glsa_…"
  },
  "tenant-b": {
    "GRAFANA_URL": "https://tenant-b.example.cloud/grafana",
    "GRAFANA_SERVICE_ACCOUNT_TOKEN": "glsa_…",
    "description": "Tenant B production"
  }
}

Optional per-instance keys: GRAFANA_ORG_ID, GRAFANA_USERNAME / GRAFANA_PASSWORD, description, extra_env, extra_args. To keep secrets out of the document itself, use GRAFANA_SERVICE_ACCOUNT_TOKEN_ENV (read from this process's environment) or GRAFANA_SERVICE_ACCOUNT_TOKEN_FILE (a path the child reads).

Auth

{
  "clients": [
    {
      "name": "claude-routines",
      "token_sha256": "3f786850e387550fdab836ed7e6dc881de23001b…",
      "instances": ["tenant-a", "tenant-b"],
      "scope": "read-only"
    },
    {
      "name": "platform-oncall",
      "token_sha256": "…",
      "instances": ["*"],
      "scope": "read-write"
    }
  ]
}

Mint a token and its hash:

grafana-unified-mcp --hash-token          # generates one
grafana-unified-mcp --hash-token 'my-existing-token'

Give token to the client; put token_sha256 in the document. Tokens are compared by digest under hmac.compare_digest, and every client is checked on every attempt so match position doesn't leak through timing.

Two things are enforced per caller:

  • instances — the instance enum a caller sees is narrowed to its grant, and a call naming an instance outside it is refused with the same message as a nonexistent one, so a token can't enumerate what it can't reach.
  • scope — read-only callers never even see mutating tools. The split comes from upstream's own readOnlyHint annotation (49 of 65 tools are read-only today), not a list maintained here, so tools added upstream are classified without a code change. Anything unannotated is treated as not read-only.

For belt-and-braces, add --child-arg=--disable-write to strip write tools at the source for every caller.

Running without authentication

--auth-mode none serves every caller that can reach the port, read-only. There is no identity to scope instances by, so all configured instances stay readable — but nothing is writable, because an open port should not be able to rewrite a dashboard or delete a snapshot. That's enforced at three layers:

  1. the published catalogue omits every mutating tool;
  2. the authorization check refuses them even if a client names one directly;
  3. children are started with --disable-write, so upstream refuses them too.

The third layer is what makes it more than a filter. Upstream swaps grafana_api_request for a separate GET-only registration — no body parameter, method narrowed to GET, non-GET rejected at runtime — so even a bug in layers 1 and 2 could not turn into a write.

stdio is different: the local caller already holds the endpoints document and every token in it, so restricting them would be theatre. stdio gets full access.

If you need writes over HTTP, use bearer tokens with a read-write client rather than an open port.

Where config comes from

Any of these, for both --endpoints and --auth:

Source Example
File /etc/grafana-unified-mcp/endpoints.json
Inline env var env:GRAFANA_ENDPOINTS_JSON
AWS Secrets Manager aws-secrets:prod/grafana/endpoints?region=us-west-2
AWS SSM Parameter Store aws-ssm:/prod/grafana/endpoints?region=us-west-2

Both documents are re-read every --config-refresh-seconds (default 300). A failed refresh logs and keeps the last good value, so a transient AWS error or a half-written file can't take the server down. Adding an instance needs no restart; removing one stops its child.

Validate before starting:

grafana-unified-mcp --endpoints … --auth … --check-config

Run

# local, over stdio (no auth — the local caller already holds the config)
grafana-unified-mcp --endpoints ./examples/endpoints.json

# deployed, over streamable-HTTP behind a reverse proxy
grafana-unified-mcp \
  --transport streamable-http \
  --address 127.0.0.1:8900 \
  --endpoints aws-secrets:prod/grafana/endpoints?region=us-west-2 \
  --auth      aws-secrets:prod/grafana/mcp-auth?region=us-west-2 \
  --public-url https://grafana-mcp.example.com

--public-url matters. The SDK applies DNS-rebinding protection based on the Host header. Behind a proxy forwarding a public hostname, that host must be allowed or every request is rejected. --public-url allows it (and is used for RFC 9728 resource metadata); --allowed-host adds more.

GET /healthz reports process health, live children, and catalog state without touching Grafana.

Connect a client

.mcp.json, for local stdio use:

{
  "mcpServers": {
    "grafana": {
      "command": "/opt/grafana-unified-mcp/.venv/bin/grafana-unified-mcp",
      "args": ["--endpoints", "/etc/grafana-unified-mcp/endpoints.json"]
    }
  }
}

For the deployed server — including Claude Code routines and cloud sessions, which is the case the bearer tokens exist for:

{
  "mcpServers": {
    "grafana": {
      "type": "http",
      "url": "https://grafana-mcp.example.com/mcp",
      "headers": {
        "Authorization": "Bearer ${GRAFANA_UNIFIED_MCP_TOKEN}"
      }
    }
  }
}

Set GRAFANA_UNIFIED_MCP_TOKEN in the environment the session runs in — for Claude Code on the web, that's the environment's variables, so scheduled routines and cloud sessions pick it up without the secret living in the repo. Give routines a read-only client; keep read-write for humans.

Deploy as a systemd service

See deploy/. In short:

sudo deploy/install.sh                       # user, dirs, venv, unit file
sudo systemctl edit grafana-unified-mcp      # set the source URIs / region
sudo systemctl enable --now grafana-unified-mcp
curl -s localhost:8900/healthz | jq

The unit runs as a dedicated unprivileged user with ProtectSystem=strict, PrivateTmp, and NoNewPrivileges. TLS terminates at nginx or an ALB in front — see deploy/nginx.conf.example, which disables response buffering (required for SSE streaming).

Using it

Point the model at list_grafana_instances first:

list_grafana_instances()
→ { "instances": [ {"name": "tenant-a", "url": "…", "connection": "live"}, … ],
    "routing_argument": "instance",
    "access": { "client": "claude-routines", "scope": "read-only" } }

Then every other tool takes that name:

search_dashboards(instance="tenant-a", query="latency")

Pass check_health=true to also probe each Grafana — slower, since it opens a connection to every instance.

One naming wrinkle

Upstream's grafana_api_request already has a required parameter called endpoint (the API path). Injecting a routing argument by that name would silently shadow it, which is why the routing argument is instance by default. If you rename it with --routing-param endpoint, that tool's own parameter is automatically republished as api_path and mapped back on the way through — no tool is ever broken by the collision, whatever you choose.

Development

uv venv && uv pip install -e '.[dev,aws]'
uv run pytest                       # unit + integration

The integration tests drive a real mcp-grafana child against an unreachable Grafana: enough to prove catalog discovery, instance injection and stripping, routing, and auth filtering, without needing live credentials. Set MCP_GRAFANA_BINARY to point at the binary, or they skip.

Roadmap

  • OAuth 2.1 — the auth layer is already an interface, and the SDK already takes an OAuth provider alongside the token verifier. Filling in OAuth2Provider.verify_token is the whole job; auth/oauth.py documents the three steps. Map IdP groups onto the existing grafana:read / grafana:write / instance:<name> scopes and every authorization check keeps working unchanged.
  • Fan-out — instance: "*" to run one read-only query across every instance and merge results. Useful for "which of these is alerting?"; left out for now because result merging deserves its own design.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选