Skip to content

Commit e1a8676

Browse files
authored
feat: add support for GRAFANA_SERVICE_ACCOUNT_TOKEN env var (#274)
1 parent 923fa3a commit e1a8676

14 files changed

+152
-48
lines changed

.github/workflows/integration.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ jobs:
6464
with:
6565
# Secrets placed in the ci/repo/grafana/mcp-grafana/<path> path in Vault
6666
repo_secrets: |
67-
GRAFANA_API_KEY=mcptests-grafana:api-key
68-
ASSERTS_GRAFANA_API_KEY=dev-grafana:api-key
67+
GRAFANA_SERVICE_ACCOUNT_TOKEN=mcptests-grafana:api-key
68+
ASSERTS_GRAFANA_SERVICE_ACCOUNT_TOKEN=dev-grafana:api-key
6969
7070
- name: Run cloud tests
7171
env:

Makefile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,12 @@ test-integration: ## Run only the Docker-based integration tests (requires docke
3737

3838
.PHONY: test-cloud
3939
test-cloud: ## Run only the cloud-based tests (requires cloud Grafana instance and credentials).
40-
ifeq ($(origin GRAFANA_API_KEY), undefined)
41-
$(error GRAFANA_API_KEY is not set. Please 'export GRAFANA_API_KEY=...' or use a tool like direnv to load it from .envrc)
40+
ifeq ($(origin GRAFANA_SERVICE_ACCOUNT_TOKEN), undefined)
41+
ifeq ($(origin GRAFANA_API_KEY), undefined)
42+
$(error Neither GRAFANA_SERVICE_ACCOUNT_TOKEN nor GRAFANA_API_KEY is set. Please 'export GRAFANA_SERVICE_ACCOUNT_TOKEN=...' or use a tool like direnv to load it from .envrc. See https://grafana.com/docs/grafana/latest/administration/service-accounts/#add-a-token-to-a-service-account-in-grafana for details on creating service account tokens.)
43+
else
44+
$(warning GRAFANA_API_KEY is deprecated, please use GRAFANA_SERVICE_ACCOUNT_TOKEN instead)
45+
endif
4246
endif
4347
GRAFANA_URL=https://mcptests.grafana-dev.net go test -v -count=1 -tags cloud ./tools
4448

README.md

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,11 @@ The `mcp-grafana` binary supports various command-line flags for configuration:
240240

241241
This MCP server works with both local Grafana instances and Grafana Cloud. For Grafana Cloud, use your instance URL (e.g., `https://myinstance.grafana.net`) instead of `http://localhost:3000` in the configuration examples below.
242242

243-
1. If using API key authentication, create a service account in Grafana with enough permissions to use the tools you want to use,
243+
1. If using service account token authentication, create a service account in Grafana with enough permissions to use the tools you want to use,
244244
generate a service account token, and copy it to the clipboard for use in the configuration file.
245-
Follow the [Grafana documentation][service-account] for details.
245+
Follow the [Grafana service account documentation][service-account] for details on creating service account tokens.
246+
247+
> **Note:** The environment variable `GRAFANA_API_KEY` is deprecated and will be removed in a future version. Please migrate to using `GRAFANA_SERVICE_ACCOUNT_TOKEN` instead. The old variable name will continue to work for backward compatibility but will show deprecation warnings.
246248
247249
2. You have several options to install `mcp-grafana`:
248250

@@ -255,23 +257,23 @@ This MCP server works with both local Grafana instances and Grafana Cloud. For G
255257
```bash
256258
docker pull mcp/grafana
257259
# For local Grafana:
258-
docker run --rm -i -e GRAFANA_URL=http://localhost:3000 -e GRAFANA_API_KEY=<your service account token> mcp/grafana -t stdio
260+
docker run --rm -i -e GRAFANA_URL=http://localhost:3000 -e GRAFANA_SERVICE_ACCOUNT_TOKEN=<your service account token> mcp/grafana -t stdio
259261
# For Grafana Cloud:
260-
docker run --rm -i -e GRAFANA_URL=https://myinstance.grafana.net -e GRAFANA_API_KEY=<your service account token> mcp/grafana -t stdio
262+
docker run --rm -i -e GRAFANA_URL=https://myinstance.grafana.net -e GRAFANA_SERVICE_ACCOUNT_TOKEN=<your service account token> mcp/grafana -t stdio
261263
```
262264

263265
2. **SSE Mode**: In this mode, the server runs as an HTTP server that clients connect to. You must expose port 8000 using the `-p` flag:
264266

265267
```bash
266268
docker pull mcp/grafana
267-
docker run --rm -p 8000:8000 -e GRAFANA_URL=http://localhost:3000 -e GRAFANA_API_KEY=<your service account token> mcp/grafana
269+
docker run --rm -p 8000:8000 -e GRAFANA_URL=http://localhost:3000 -e GRAFANA_SERVICE_ACCOUNT_TOKEN=<your service account token> mcp/grafana
268270
```
269271

270272
3. **Streamable HTTP Mode**: In this mode, the server operates as an independent process that can handle multiple client connections. You must expose port 8000 using the `-p` flag: For this mode you must explicitly override the default with `-t streamable-http`
271273

272274
```bash
273275
docker pull mcp/grafana
274-
docker run --rm -p 8000:8000 -e GRAFANA_URL=http://localhost:3000 -e GRAFANA_API_KEY=<your service account token> mcp/grafana -t streamable-http
276+
docker run --rm -p 8000:8000 -e GRAFANA_URL=http://localhost:3000 -e GRAFANA_SERVICE_ACCOUNT_TOKEN=<your service account token> mcp/grafana -t streamable-http
275277
```
276278

277279
For HTTPS streamable HTTP mode with server TLS certificates:
@@ -281,7 +283,7 @@ This MCP server works with both local Grafana instances and Grafana Cloud. For G
281283
docker run --rm -p 8443:8443 \
282284
-v /path/to/certs:/certs:ro \
283285
-e GRAFANA_URL=http://localhost:3000 \
284-
-e GRAFANA_API_KEY=<your service account token> \
286+
-e GRAFANA_SERVICE_ACCOUNT_TOKEN=<your service account token> \
285287
mcp/grafana \
286288
-t streamable-http \
287289
-addr :8443 \
@@ -318,7 +320,7 @@ This MCP server works with both local Grafana instances and Grafana Cloud. For G
318320
"args": [],
319321
"env": {
320322
"GRAFANA_URL": "http://localhost:3000", // Or "https://myinstance.grafana.net" for Grafana Cloud
321-
"GRAFANA_API_KEY": "<your service account token>",
323+
"GRAFANA_SERVICE_ACCOUNT_TOKEN": "<your service account token>",
322324
// If using username/password authentication
323325
"GRAFANA_USERNAME": "<your username>",
324326
"GRAFANA_PASSWORD": "<your password>"
@@ -344,14 +346,14 @@ This MCP server works with both local Grafana instances and Grafana Cloud. For G
344346
"-e",
345347
"GRAFANA_URL",
346348
"-e",
347-
"GRAFANA_API_KEY",
349+
"GRAFANA_SERVICE_ACCOUNT_TOKEN",
348350
"mcp/grafana",
349351
"-t",
350352
"stdio"
351353
],
352354
"env": {
353355
"GRAFANA_URL": "http://localhost:3000", // Or "https://myinstance.grafana.net" for Grafana Cloud
354-
"GRAFANA_API_KEY": "<your service account token>",
356+
"GRAFANA_SERVICE_ACCOUNT_TOKEN": "<your service account token>",
355357
// If using username/password authentication
356358
"GRAFANA_USERNAME": "<your username>",
357359
"GRAFANA_PASSWORD": "<your password>"
@@ -407,7 +409,7 @@ To use debug mode with the Claude Desktop configuration, update your config as f
407409
"args": ["-debug"],
408410
"env": {
409411
"GRAFANA_URL": "http://localhost:3000", // Or "https://myinstance.grafana.net" for Grafana Cloud
410-
"GRAFANA_API_KEY": "<your service account token>"
412+
"GRAFANA_SERVICE_ACCOUNT_TOKEN": "<your service account token>"
411413
}
412414
}
413415
}
@@ -428,15 +430,15 @@ To use debug mode with the Claude Desktop configuration, update your config as f
428430
"-e",
429431
"GRAFANA_URL",
430432
"-e",
431-
"GRAFANA_API_KEY",
433+
"GRAFANA_SERVICE_ACCOUNT_TOKEN",
432434
"mcp/grafana",
433435
"-t",
434436
"stdio",
435437
"-debug"
436438
],
437439
"env": {
438440
"GRAFANA_URL": "http://localhost:3000", // Or "https://myinstance.grafana.net" for Grafana Cloud
439-
"GRAFANA_API_KEY": "<your service account token>"
441+
"GRAFANA_SERVICE_ACCOUNT_TOKEN": "<your service account token>"
440442
}
441443
}
442444
}
@@ -471,7 +473,7 @@ If your Grafana instance is behind mTLS or requires custom TLS certificates, you
471473
],
472474
"env": {
473475
"GRAFANA_URL": "https://secure-grafana.example.com",
474-
"GRAFANA_API_KEY": "<your service account token>"
476+
"GRAFANA_SERVICE_ACCOUNT_TOKEN": "<your service account token>"
475477
}
476478
}
477479
}
@@ -494,7 +496,7 @@ If your Grafana instance is behind mTLS or requires custom TLS certificates, you
494496
"-e",
495497
"GRAFANA_URL",
496498
"-e",
497-
"GRAFANA_API_KEY",
499+
"GRAFANA_SERVICE_ACCOUNT_TOKEN",
498500
"mcp/grafana",
499501
"-t",
500502
"stdio",
@@ -507,7 +509,7 @@ If your Grafana instance is behind mTLS or requires custom TLS certificates, you
507509
],
508510
"env": {
509511
"GRAFANA_URL": "https://secure-grafana.example.com",
510-
"GRAFANA_API_KEY": "<your service account token>"
512+
"GRAFANA_SERVICE_ACCOUNT_TOKEN": "<your service account token>"
511513
}
512514
}
513515
}
@@ -606,7 +608,7 @@ This would start the MCP server on HTTPS port 8443. Clients would then connect t
606608
docker run --rm -p 8443:8443 \
607609
-v /path/to/certs:/certs:ro \
608610
-e GRAFANA_URL=http://localhost:3000 \
609-
-e GRAFANA_API_KEY=<your service account token> \
611+
-e GRAFANA_SERVICE_ACCOUNT_TOKEN=<your service account token> \
610612
mcp/grafana \
611613
-t streamable-http \
612614
-addr :8443 \
@@ -729,4 +731,4 @@ See the [JSONSchema Linter documentation](internal/linter/jsonschema/README.md)
729731
This project is licensed under the [Apache License, Version 2.0](LICENSE).
730732
731733
[mcp]: https://modelcontextprotocol.io/
732-
[service-account]: https://grafana.com/docs/grafana/latest/administration/service-accounts/
734+
[service-account]: https://grafana.com/docs/grafana/latest/administration/service-accounts/#add-a-token-to-a-service-account-in-grafana

examples/tls_example.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,13 @@ func runServerWithTLS() {
134134
log.Printf("Failed to set GRAFANA_URL: %v", err)
135135
}
136136
}
137-
if os.Getenv("GRAFANA_API_KEY") == "" {
138-
fmt.Println("Warning: GRAFANA_API_KEY not set")
137+
// Check for service account token first, then fall back to deprecated API key
138+
if os.Getenv("GRAFANA_SERVICE_ACCOUNT_TOKEN") == "" {
139+
if os.Getenv("GRAFANA_API_KEY") == "" {
140+
fmt.Println("Warning: Neither GRAFANA_SERVICE_ACCOUNT_TOKEN nor GRAFANA_API_KEY is set")
141+
} else {
142+
fmt.Println("Warning: GRAFANA_API_KEY is deprecated, please use GRAFANA_SERVICE_ACCOUNT_TOKEN instead")
143+
}
139144
}
140145

141146
// Create TLS configuration that skips verification for demo purposes

mcpgrafana.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ const (
2525
defaultGrafanaHost = "localhost:3000"
2626
defaultGrafanaURL = "http://" + defaultGrafanaHost
2727

28-
grafanaURLEnvVar = "GRAFANA_URL"
29-
grafanaAPIEnvVar = "GRAFANA_API_KEY"
28+
grafanaURLEnvVar = "GRAFANA_URL"
29+
grafanaServiceAccountTokenEnvVar = "GRAFANA_SERVICE_ACCOUNT_TOKEN"
30+
grafanaAPIEnvVar = "GRAFANA_API_KEY" // Deprecated: use GRAFANA_SERVICE_ACCOUNT_TOKEN instead
3031

3132
grafanaUsernameEnvVar = "GRAFANA_USERNAME"
3233
grafanaPasswordEnvVar = "GRAFANA_PASSWORD"
@@ -37,7 +38,19 @@ const (
3738

3839
func urlAndAPIKeyFromEnv() (string, string) {
3940
u := strings.TrimRight(os.Getenv(grafanaURLEnvVar), "/")
40-
apiKey := os.Getenv(grafanaAPIEnvVar)
41+
42+
// Check for the new service account token environment variable first
43+
apiKey := os.Getenv(grafanaServiceAccountTokenEnvVar)
44+
if apiKey != "" {
45+
return u, apiKey
46+
}
47+
48+
// Fall back to the deprecated API key environment variable
49+
apiKey = os.Getenv(grafanaAPIEnvVar)
50+
if apiKey != "" {
51+
slog.Warn("GRAFANA_API_KEY is deprecated, please use GRAFANA_SERVICE_ACCOUNT_TOKEN instead. See https://grafana.com/docs/grafana/latest/administration/service-accounts/#add-a-token-to-a-service-account-in-grafana for details on creating service account tokens.")
52+
}
53+
4154
return u, apiKey
4255
}
4356

@@ -271,7 +284,7 @@ func extractKeyGrafanaInfoFromReq(req *http.Request) (grafanaUrl, apiKey string,
271284
}
272285

273286
// ExtractGrafanaInfoFromEnv is a StdioContextFunc that extracts Grafana configuration from environment variables.
274-
// It reads GRAFANA_URL and GRAFANA_API_KEY environment variables and adds the configuration to the context for use by Grafana clients.
287+
// It reads GRAFANA_URL and GRAFANA_SERVICE_ACCOUNT_TOKEN (or deprecated GRAFANA_API_KEY) environment variables and adds the configuration to the context for use by Grafana clients.
275288
var ExtractGrafanaInfoFromEnv server.StdioContextFunc = func(ctx context.Context) context.Context {
276289
u, apiKey, basicAuth := extractKeyGrafanaInfoFromEnv()
277290
parsedURL, err := url.Parse(u)
@@ -412,7 +425,7 @@ func NewGrafanaClient(ctx context.Context, grafanaURL, apiKey string, auth *url.
412425
}
413426

414427
// ExtractGrafanaClientFromEnv is a StdioContextFunc that creates and injects a Grafana client into the context.
415-
// It uses configuration from GRAFANA_URL, GRAFANA_API_KEY, GRAFANA_USERNAME/PASSWORD environment variables to initialize
428+
// It uses configuration from GRAFANA_URL, GRAFANA_SERVICE_ACCOUNT_TOKEN (or deprecated GRAFANA_API_KEY), GRAFANA_USERNAME/PASSWORD environment variables to initialize
416429
// the client with proper authentication.
417430
var ExtractGrafanaClientFromEnv server.StdioContextFunc = func(ctx context.Context) context.Context {
418431
// Extract transport config from env vars

mcpgrafana_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ func TestExtractGrafanaInfoFromHeaders(t *testing.T) {
8080
// Explicitly clear environment variables to ensure test isolation
8181
t.Setenv("GRAFANA_URL", "")
8282
t.Setenv("GRAFANA_API_KEY", "")
83+
t.Setenv("GRAFANA_SERVICE_ACCOUNT_TOKEN", "")
8384

8485
req, err := http.NewRequest("GET", "http://example.com", nil)
8586
require.NoError(t, err)
@@ -102,6 +103,31 @@ func TestExtractGrafanaInfoFromHeaders(t *testing.T) {
102103
assert.Equal(t, "my-test-api-key", config.APIKey)
103104
})
104105

106+
t.Run("no headers, with service account token", func(t *testing.T) {
107+
t.Setenv("GRAFANA_URL", "http://my-test-url.grafana.com")
108+
t.Setenv("GRAFANA_SERVICE_ACCOUNT_TOKEN", "my-service-account-token")
109+
110+
req, err := http.NewRequest("GET", "http://example.com", nil)
111+
require.NoError(t, err)
112+
ctx := ExtractGrafanaInfoFromHeaders(context.Background(), req)
113+
config := GrafanaConfigFromContext(ctx)
114+
assert.Equal(t, "http://my-test-url.grafana.com", config.URL)
115+
assert.Equal(t, "my-service-account-token", config.APIKey)
116+
})
117+
118+
t.Run("no headers, service account token takes precedence over api key", func(t *testing.T) {
119+
t.Setenv("GRAFANA_URL", "http://my-test-url.grafana.com")
120+
t.Setenv("GRAFANA_API_KEY", "my-deprecated-api-key")
121+
t.Setenv("GRAFANA_SERVICE_ACCOUNT_TOKEN", "my-service-account-token")
122+
123+
req, err := http.NewRequest("GET", "http://example.com", nil)
124+
require.NoError(t, err)
125+
ctx := ExtractGrafanaInfoFromHeaders(context.Background(), req)
126+
config := GrafanaConfigFromContext(ctx)
127+
assert.Equal(t, "http://my-test-url.grafana.com", config.URL)
128+
assert.Equal(t, "my-service-account-token", config.APIKey)
129+
})
130+
105131
t.Run("with headers, no env", func(t *testing.T) {
106132
req, err := http.NewRequest("GET", "http://example.com", nil)
107133
require.NoError(t, err)
@@ -117,6 +143,7 @@ func TestExtractGrafanaInfoFromHeaders(t *testing.T) {
117143
// Env vars should be ignored if headers are present.
118144
t.Setenv("GRAFANA_URL", "will-not-be-used")
119145
t.Setenv("GRAFANA_API_KEY", "will-not-be-used")
146+
t.Setenv("GRAFANA_SERVICE_ACCOUNT_TOKEN", "will-not-be-used")
120147

121148
req, err := http.NewRequest("GET", "http://example.com", nil)
122149
require.NoError(t, err)

tests/admin_test.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,21 @@ async def grafana_team():
2727
# Generate a unique team name to avoid conflicts
2828
team_name = f"test-team-{uuid.uuid4().hex[:8]}"
2929

30-
# Get Grafana URL and API key from environment
30+
# Get Grafana URL and service account token from environment
3131
grafana_url = os.environ.get("GRAFANA_URL", DEFAULT_GRAFANA_URL)
3232

3333
auth_header = None
34-
if api_key := os.environ.get("GRAFANA_API_KEY"):
34+
# Check for the new service account token environment variable first
35+
if api_key := os.environ.get("GRAFANA_SERVICE_ACCOUNT_TOKEN"):
3536
auth_header = {"Authorization": f"Bearer {api_key}"}
37+
elif api_key := os.environ.get("GRAFANA_API_KEY"):
38+
auth_header = {"Authorization": f"Bearer {api_key}"}
39+
import warnings
40+
41+
warnings.warn(
42+
"GRAFANA_API_KEY is deprecated, please use GRAFANA_SERVICE_ACCOUNT_TOKEN instead. See https://grafana.com/docs/grafana/latest/administration/service-accounts/#add-a-token-to-a-service-account-in-grafana for details on creating service account tokens.",
43+
DeprecationWarning,
44+
)
3645

3746
if not auth_header:
3847
pytest.skip("No authentication credentials available to create team")

tests/conftest.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,20 @@ def mcp_url():
4646
@pytest.fixture
4747
def grafana_env():
4848
env = {"GRAFANA_URL": os.environ.get("GRAFANA_URL", DEFAULT_GRAFANA_URL)}
49-
if key := os.environ.get("GRAFANA_API_KEY"):
49+
# Check for the new service account token environment variable first
50+
if key := os.environ.get("GRAFANA_SERVICE_ACCOUNT_TOKEN"):
51+
env["GRAFANA_SERVICE_ACCOUNT_TOKEN"] = key
52+
elif key := os.environ.get("GRAFANA_API_KEY"):
5053
env["GRAFANA_API_KEY"] = key
51-
elif (username := os.environ.get("GRAFANA_USERNAME")) and (password := os.environ.get("GRAFANA_USERNAME")):
54+
import warnings
55+
56+
warnings.warn(
57+
"GRAFANA_API_KEY is deprecated, please use GRAFANA_SERVICE_ACCOUNT_TOKEN instead. See https://grafana.com/docs/grafana/latest/administration/service-accounts/#add-a-token-to-a-service-account-in-grafana for details on creating service account tokens.",
58+
DeprecationWarning,
59+
)
60+
elif (username := os.environ.get("GRAFANA_USERNAME")) and (
61+
password := os.environ.get("GRAFANA_PASSWORD")
62+
):
5263
env["GRAFANA_USERNAME"] = username
5364
env["GRAFANA_PASSWORD"] = password
5465
return env
@@ -59,11 +70,24 @@ def grafana_headers():
5970
headers = {
6071
"X-Grafana-URL": os.environ.get("GRAFANA_URL", DEFAULT_GRAFANA_URL),
6172
}
62-
if key := os.environ.get("GRAFANA_API_KEY"):
73+
# Check for the new service account token environment variable first
74+
if key := os.environ.get("GRAFANA_SERVICE_ACCOUNT_TOKEN"):
75+
headers["X-Grafana-API-Key"] = key
76+
elif key := os.environ.get("GRAFANA_API_KEY"):
6377
headers["X-Grafana-API-Key"] = key
64-
elif (username := os.environ.get("GRAFANA_USERNAME")) and (password := os.environ.get("GRAFANA_PASSWORD")):
78+
import warnings
79+
80+
warnings.warn(
81+
"GRAFANA_API_KEY is deprecated, please use GRAFANA_SERVICE_ACCOUNT_TOKEN instead. See https://grafana.com/docs/grafana/latest/administration/service-accounts/#add-a-token-to-a-service-account-in-grafana for details on creating service account tokens.",
82+
DeprecationWarning,
83+
)
84+
elif (username := os.environ.get("GRAFANA_USERNAME")) and (
85+
password := os.environ.get("GRAFANA_PASSWORD")
86+
):
6587
credentials = f"{username}:{password}"
66-
headers["Authorization"] = "Basic " + base64.b64encode(credentials.encode("utf-8")).decode()
88+
headers["Authorization"] = (
89+
"Basic " + base64.b64encode(credentials.encode("utf-8")).decode()
90+
)
6791
return headers
6892

6993

tools/asserts_cloud_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
// +build cloud
33

44
// This file contains cloud integration tests that run against a dedicated test instance
5-
// connected to a Grafana instance at (ASSERTS_GRAFANA_URL, ASSERTS_GRAFANA_API_KEY).
5+
// connected to a Grafana instance at (ASSERTS_GRAFANA_URL, ASSERTS_GRAFANA_SERVICE_ACCOUNT_TOKEN or ASSERTS_GRAFANA_API_KEY).
66
// These tests expect this configuration to exist and will skip if the required
7-
// environment variables are not set.
7+
// environment variables are not set. The ASSERTS_GRAFANA_API_KEY variable is deprecated.
88

99
package tools
1010

0 commit comments

Comments
 (0)