Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ jobs:
with:
# Secrets placed in the ci/repo/grafana/mcp-grafana/<path> path in Vault
repo_secrets: |
GRAFANA_API_KEY=mcptests-grafana:api-key
ASSERTS_GRAFANA_API_KEY=dev-grafana:api-key
GRAFANA_SERVICE_ACCOUNT_TOKEN=mcptests-grafana:api-key
ASSERTS_GRAFANA_SERVICE_ACCOUNT_TOKEN=dev-grafana:api-key

- name: Run cloud tests
env:
Expand Down
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,12 @@ test-integration: ## Run only the Docker-based integration tests (requires docke

.PHONY: test-cloud
test-cloud: ## Run only the cloud-based tests (requires cloud Grafana instance and credentials).
ifeq ($(origin GRAFANA_API_KEY), undefined)
$(error GRAFANA_API_KEY is not set. Please 'export GRAFANA_API_KEY=...' or use a tool like direnv to load it from .envrc)
ifeq ($(origin GRAFANA_SERVICE_ACCOUNT_TOKEN), undefined)
ifeq ($(origin GRAFANA_API_KEY), undefined)
$(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.)
else
$(warning GRAFANA_API_KEY is deprecated, please use GRAFANA_SERVICE_ACCOUNT_TOKEN instead)
endif
endif
GRAFANA_URL=https://mcptests.grafana-dev.net go test -v -count=1 -tags cloud ./tools

Expand Down
38 changes: 20 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,9 +240,11 @@ The `mcp-grafana` binary supports various command-line flags for configuration:

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.

1. If using API key authentication, create a service account in Grafana with enough permissions to use the tools you want to use,
1. If using service account token authentication, create a service account in Grafana with enough permissions to use the tools you want to use,
generate a service account token, and copy it to the clipboard for use in the configuration file.
Follow the [Grafana documentation][service-account] for details.
Follow the [Grafana service account documentation][service-account] for details on creating service account tokens.

> **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.

2. You have several options to install `mcp-grafana`:

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

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:

```bash
docker pull mcp/grafana
docker run --rm -p 8000:8000 -e GRAFANA_URL=http://localhost:3000 -e GRAFANA_API_KEY=<your service account token> mcp/grafana
docker run --rm -p 8000:8000 -e GRAFANA_URL=http://localhost:3000 -e GRAFANA_SERVICE_ACCOUNT_TOKEN=<your service account token> mcp/grafana
```

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`

```bash
docker pull mcp/grafana
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
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
```

For HTTPS streamable HTTP mode with server TLS certificates:
Expand All @@ -281,7 +283,7 @@ This MCP server works with both local Grafana instances and Grafana Cloud. For G
docker run --rm -p 8443:8443 \
-v /path/to/certs:/certs:ro \
-e GRAFANA_URL=http://localhost:3000 \
-e GRAFANA_API_KEY=<your service account token> \
-e GRAFANA_SERVICE_ACCOUNT_TOKEN=<your service account token> \
mcp/grafana \
-t streamable-http \
-addr :8443 \
Expand Down Expand Up @@ -318,7 +320,7 @@ This MCP server works with both local Grafana instances and Grafana Cloud. For G
"args": [],
"env": {
"GRAFANA_URL": "http://localhost:3000", // Or "https://myinstance.grafana.net" for Grafana Cloud
"GRAFANA_API_KEY": "<your service account token>",
"GRAFANA_SERVICE_ACCOUNT_TOKEN": "<your service account token>",
// If using username/password authentication
"GRAFANA_USERNAME": "<your username>",
"GRAFANA_PASSWORD": "<your password>"
Expand All @@ -344,14 +346,14 @@ This MCP server works with both local Grafana instances and Grafana Cloud. For G
"-e",
"GRAFANA_URL",
"-e",
"GRAFANA_API_KEY",
"GRAFANA_SERVICE_ACCOUNT_TOKEN",
"mcp/grafana",
"-t",
"stdio"
],
"env": {
"GRAFANA_URL": "http://localhost:3000", // Or "https://myinstance.grafana.net" for Grafana Cloud
"GRAFANA_API_KEY": "<your service account token>",
"GRAFANA_SERVICE_ACCOUNT_TOKEN": "<your service account token>",
// If using username/password authentication
"GRAFANA_USERNAME": "<your username>",
"GRAFANA_PASSWORD": "<your password>"
Expand Down Expand Up @@ -407,7 +409,7 @@ To use debug mode with the Claude Desktop configuration, update your config as f
"args": ["-debug"],
"env": {
"GRAFANA_URL": "http://localhost:3000", // Or "https://myinstance.grafana.net" for Grafana Cloud
"GRAFANA_API_KEY": "<your service account token>"
"GRAFANA_SERVICE_ACCOUNT_TOKEN": "<your service account token>"
}
}
}
Expand All @@ -428,15 +430,15 @@ To use debug mode with the Claude Desktop configuration, update your config as f
"-e",
"GRAFANA_URL",
"-e",
"GRAFANA_API_KEY",
"GRAFANA_SERVICE_ACCOUNT_TOKEN",
"mcp/grafana",
"-t",
"stdio",
"-debug"
],
"env": {
"GRAFANA_URL": "http://localhost:3000", // Or "https://myinstance.grafana.net" for Grafana Cloud
"GRAFANA_API_KEY": "<your service account token>"
"GRAFANA_SERVICE_ACCOUNT_TOKEN": "<your service account token>"
}
}
}
Expand Down Expand Up @@ -471,7 +473,7 @@ If your Grafana instance is behind mTLS or requires custom TLS certificates, you
],
"env": {
"GRAFANA_URL": "https://secure-grafana.example.com",
"GRAFANA_API_KEY": "<your service account token>"
"GRAFANA_SERVICE_ACCOUNT_TOKEN": "<your service account token>"
}
}
}
Expand All @@ -494,7 +496,7 @@ If your Grafana instance is behind mTLS or requires custom TLS certificates, you
"-e",
"GRAFANA_URL",
"-e",
"GRAFANA_API_KEY",
"GRAFANA_SERVICE_ACCOUNT_TOKEN",
"mcp/grafana",
"-t",
"stdio",
Expand All @@ -507,7 +509,7 @@ If your Grafana instance is behind mTLS or requires custom TLS certificates, you
],
"env": {
"GRAFANA_URL": "https://secure-grafana.example.com",
"GRAFANA_API_KEY": "<your service account token>"
"GRAFANA_SERVICE_ACCOUNT_TOKEN": "<your service account token>"
}
}
}
Expand Down Expand Up @@ -606,7 +608,7 @@ This would start the MCP server on HTTPS port 8443. Clients would then connect t
docker run --rm -p 8443:8443 \
-v /path/to/certs:/certs:ro \
-e GRAFANA_URL=http://localhost:3000 \
-e GRAFANA_API_KEY=<your service account token> \
-e GRAFANA_SERVICE_ACCOUNT_TOKEN=<your service account token> \
mcp/grafana \
-t streamable-http \
-addr :8443 \
Expand Down Expand Up @@ -729,4 +731,4 @@ See the [JSONSchema Linter documentation](internal/linter/jsonschema/README.md)
This project is licensed under the [Apache License, Version 2.0](LICENSE).

[mcp]: https://modelcontextprotocol.io/
[service-account]: https://grafana.com/docs/grafana/latest/administration/service-accounts/
[service-account]: https://grafana.com/docs/grafana/latest/administration/service-accounts/#add-a-token-to-a-service-account-in-grafana
9 changes: 7 additions & 2 deletions examples/tls_example.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,13 @@ func runServerWithTLS() {
log.Printf("Failed to set GRAFANA_URL: %v", err)
}
}
if os.Getenv("GRAFANA_API_KEY") == "" {
fmt.Println("Warning: GRAFANA_API_KEY not set")
// Check for service account token first, then fall back to deprecated API key
if os.Getenv("GRAFANA_SERVICE_ACCOUNT_TOKEN") == "" {
if os.Getenv("GRAFANA_API_KEY") == "" {
fmt.Println("Warning: Neither GRAFANA_SERVICE_ACCOUNT_TOKEN nor GRAFANA_API_KEY is set")
} else {
fmt.Println("Warning: GRAFANA_API_KEY is deprecated, please use GRAFANA_SERVICE_ACCOUNT_TOKEN instead")
}
}

// Create TLS configuration that skips verification for demo purposes
Expand Down
23 changes: 18 additions & 5 deletions mcpgrafana.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ const (
defaultGrafanaHost = "localhost:3000"
defaultGrafanaURL = "http://" + defaultGrafanaHost

grafanaURLEnvVar = "GRAFANA_URL"
grafanaAPIEnvVar = "GRAFANA_API_KEY"
grafanaURLEnvVar = "GRAFANA_URL"
grafanaServiceAccountTokenEnvVar = "GRAFANA_SERVICE_ACCOUNT_TOKEN"
grafanaAPIEnvVar = "GRAFANA_API_KEY" // Deprecated: use GRAFANA_SERVICE_ACCOUNT_TOKEN instead

grafanaUsernameEnvVar = "GRAFANA_USERNAME"
grafanaPasswordEnvVar = "GRAFANA_PASSWORD"
Expand All @@ -37,7 +38,19 @@ const (

func urlAndAPIKeyFromEnv() (string, string) {
u := strings.TrimRight(os.Getenv(grafanaURLEnvVar), "/")
apiKey := os.Getenv(grafanaAPIEnvVar)

// Check for the new service account token environment variable first
apiKey := os.Getenv(grafanaServiceAccountTokenEnvVar)
if apiKey != "" {
return u, apiKey
}

// Fall back to the deprecated API key environment variable
apiKey = os.Getenv(grafanaAPIEnvVar)
if apiKey != "" {
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.")
}

return u, apiKey
}

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

// ExtractGrafanaInfoFromEnv is a StdioContextFunc that extracts Grafana configuration from environment variables.
// It reads GRAFANA_URL and GRAFANA_API_KEY environment variables and adds the configuration to the context for use by Grafana clients.
// 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.
var ExtractGrafanaInfoFromEnv server.StdioContextFunc = func(ctx context.Context) context.Context {
u, apiKey, basicAuth := extractKeyGrafanaInfoFromEnv()
parsedURL, err := url.Parse(u)
Expand Down Expand Up @@ -412,7 +425,7 @@ func NewGrafanaClient(ctx context.Context, grafanaURL, apiKey string, auth *url.
}

// ExtractGrafanaClientFromEnv is a StdioContextFunc that creates and injects a Grafana client into the context.
// It uses configuration from GRAFANA_URL, GRAFANA_API_KEY, GRAFANA_USERNAME/PASSWORD environment variables to initialize
// It uses configuration from GRAFANA_URL, GRAFANA_SERVICE_ACCOUNT_TOKEN (or deprecated GRAFANA_API_KEY), GRAFANA_USERNAME/PASSWORD environment variables to initialize
// the client with proper authentication.
var ExtractGrafanaClientFromEnv server.StdioContextFunc = func(ctx context.Context) context.Context {
// Extract transport config from env vars
Expand Down
27 changes: 27 additions & 0 deletions mcpgrafana_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ func TestExtractGrafanaInfoFromHeaders(t *testing.T) {
// Explicitly clear environment variables to ensure test isolation
t.Setenv("GRAFANA_URL", "")
t.Setenv("GRAFANA_API_KEY", "")
t.Setenv("GRAFANA_SERVICE_ACCOUNT_TOKEN", "")

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

t.Run("no headers, with service account token", func(t *testing.T) {
t.Setenv("GRAFANA_URL", "http://my-test-url.grafana.com")
t.Setenv("GRAFANA_SERVICE_ACCOUNT_TOKEN", "my-service-account-token")

req, err := http.NewRequest("GET", "http://example.com", nil)
require.NoError(t, err)
ctx := ExtractGrafanaInfoFromHeaders(context.Background(), req)
config := GrafanaConfigFromContext(ctx)
assert.Equal(t, "http://my-test-url.grafana.com", config.URL)
assert.Equal(t, "my-service-account-token", config.APIKey)
})

t.Run("no headers, service account token takes precedence over api key", func(t *testing.T) {
t.Setenv("GRAFANA_URL", "http://my-test-url.grafana.com")
t.Setenv("GRAFANA_API_KEY", "my-deprecated-api-key")
t.Setenv("GRAFANA_SERVICE_ACCOUNT_TOKEN", "my-service-account-token")

req, err := http.NewRequest("GET", "http://example.com", nil)
require.NoError(t, err)
ctx := ExtractGrafanaInfoFromHeaders(context.Background(), req)
config := GrafanaConfigFromContext(ctx)
assert.Equal(t, "http://my-test-url.grafana.com", config.URL)
assert.Equal(t, "my-service-account-token", config.APIKey)
})

t.Run("with headers, no env", func(t *testing.T) {
req, err := http.NewRequest("GET", "http://example.com", nil)
require.NoError(t, err)
Expand All @@ -117,6 +143,7 @@ func TestExtractGrafanaInfoFromHeaders(t *testing.T) {
// Env vars should be ignored if headers are present.
t.Setenv("GRAFANA_URL", "will-not-be-used")
t.Setenv("GRAFANA_API_KEY", "will-not-be-used")
t.Setenv("GRAFANA_SERVICE_ACCOUNT_TOKEN", "will-not-be-used")

req, err := http.NewRequest("GET", "http://example.com", nil)
require.NoError(t, err)
Expand Down
13 changes: 11 additions & 2 deletions tests/admin_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,21 @@ async def grafana_team():
# Generate a unique team name to avoid conflicts
team_name = f"test-team-{uuid.uuid4().hex[:8]}"

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

auth_header = None
if api_key := os.environ.get("GRAFANA_API_KEY"):
# Check for the new service account token environment variable first
if api_key := os.environ.get("GRAFANA_SERVICE_ACCOUNT_TOKEN"):
auth_header = {"Authorization": f"Bearer {api_key}"}
elif api_key := os.environ.get("GRAFANA_API_KEY"):
auth_header = {"Authorization": f"Bearer {api_key}"}
import warnings

warnings.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.",
DeprecationWarning,
)

if not auth_header:
pytest.skip("No authentication credentials available to create team")
Expand Down
34 changes: 29 additions & 5 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,20 @@ def mcp_url():
@pytest.fixture
def grafana_env():
env = {"GRAFANA_URL": os.environ.get("GRAFANA_URL", DEFAULT_GRAFANA_URL)}
if key := os.environ.get("GRAFANA_API_KEY"):
# Check for the new service account token environment variable first
if key := os.environ.get("GRAFANA_SERVICE_ACCOUNT_TOKEN"):
env["GRAFANA_SERVICE_ACCOUNT_TOKEN"] = key
elif key := os.environ.get("GRAFANA_API_KEY"):
env["GRAFANA_API_KEY"] = key
elif (username := os.environ.get("GRAFANA_USERNAME")) and (password := os.environ.get("GRAFANA_USERNAME")):
import warnings

warnings.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.",
DeprecationWarning,
)
elif (username := os.environ.get("GRAFANA_USERNAME")) and (
password := os.environ.get("GRAFANA_PASSWORD")
):
env["GRAFANA_USERNAME"] = username
env["GRAFANA_PASSWORD"] = password
return env
Expand All @@ -59,11 +70,24 @@ def grafana_headers():
headers = {
"X-Grafana-URL": os.environ.get("GRAFANA_URL", DEFAULT_GRAFANA_URL),
}
if key := os.environ.get("GRAFANA_API_KEY"):
# Check for the new service account token environment variable first
if key := os.environ.get("GRAFANA_SERVICE_ACCOUNT_TOKEN"):
headers["X-Grafana-API-Key"] = key
elif key := os.environ.get("GRAFANA_API_KEY"):
headers["X-Grafana-API-Key"] = key
elif (username := os.environ.get("GRAFANA_USERNAME")) and (password := os.environ.get("GRAFANA_PASSWORD")):
import warnings

warnings.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.",
DeprecationWarning,
)
elif (username := os.environ.get("GRAFANA_USERNAME")) and (
password := os.environ.get("GRAFANA_PASSWORD")
):
credentials = f"{username}:{password}"
headers["Authorization"] = "Basic " + base64.b64encode(credentials.encode("utf-8")).decode()
headers["Authorization"] = (
"Basic " + base64.b64encode(credentials.encode("utf-8")).decode()
)
return headers


Expand Down
4 changes: 2 additions & 2 deletions tools/asserts_cloud_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
// +build cloud

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

package tools

Expand Down
Loading