Skip to content

Commit 1a7f9c7

Browse files
authored
refactor: consolidate Grafana configuration into GrafanaConfig struct (#168)
1 parent 2a8b07e commit 1a7f9c7

15 files changed

+145
-151
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ _The following features are currently available in MCP server. This list is for
1111
### Dashboards
1212
- **Search for dashboards:** Find dashboards by title or other metadata
1313
- **Get dashboard by UID:** Retrieve full dashboard details using its unique identifier
14-
- **Update or create a dashboard:** Modify existing dashboards or create new ones. _Note: Use with caution due to context window limitations; see [issue #101](https://github.com/grafana/mcp-grafana/issues/101)_
14+
- **Update or create a dashboard:** Modify existing dashboards or create new ones. _Note: Use with caution due to context window limitations; see [issue #101](https://github.com/grafana/mcp-grafana/issues/101)_
1515
- **Get panel queries and datasource info:** Get the title, query string, and datasource information (including UID and type, if available) from every panel in a dashboard
1616

1717
### Datasources

cmd/linters/jsonschema/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,4 @@ func main() {
5454
if len(jsonLinter.Errors) > 0 {
5555
os.Exit(1)
5656
}
57-
}
57+
}

cmd/mcp-grafana/main.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,27 +87,27 @@ func newServer(dt disabledTools) *server.MCPServer {
8787
return s
8888
}
8989

90-
func run(transport, addr, basePath string, endpointPath string, logLevel slog.Level, dt disabledTools, gc grafanaConfig) error {
90+
func run(transport, addr, basePath, endpointPath string, logLevel slog.Level, dt disabledTools, gc mcpgrafana.GrafanaConfig) error {
9191
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: logLevel})))
9292
s := newServer(dt)
9393

9494
switch transport {
9595
case "stdio":
9696
srv := server.NewStdioServer(s)
97-
srv.SetContextFunc(mcpgrafana.ComposedStdioContextFunc(gc.debug))
97+
srv.SetContextFunc(mcpgrafana.ComposedStdioContextFunc(gc))
9898
slog.Info("Starting Grafana MCP server using stdio transport")
9999
return srv.Listen(context.Background(), os.Stdin, os.Stdout)
100100
case "sse":
101101
srv := server.NewSSEServer(s,
102-
server.WithSSEContextFunc(mcpgrafana.ComposedSSEContextFunc(gc.debug)),
102+
server.WithSSEContextFunc(mcpgrafana.ComposedSSEContextFunc(gc)),
103103
server.WithStaticBasePath(basePath),
104104
)
105105
slog.Info("Starting Grafana MCP server using SSE transport", "address", addr, "basePath", basePath)
106106
if err := srv.Start(addr); err != nil {
107107
return fmt.Errorf("Server error: %v", err)
108108
}
109109
case "streamable-http":
110-
srv := server.NewStreamableHTTPServer(s, server.WithHTTPContextFunc(mcpgrafana.ComposedHTTPContextFunc(gc.debug)),
110+
srv := server.NewStreamableHTTPServer(s, server.WithHTTPContextFunc(mcpgrafana.ComposedHTTPContextFunc(gc)),
111111
server.WithStateLess(true),
112112
server.WithEndpointPath(endpointPath),
113113
)
@@ -143,7 +143,9 @@ func main() {
143143
gc.addFlags()
144144
flag.Parse()
145145

146-
if err := run(transport, *addr, *basePath, *endpointPath, parseLevel(*logLevel), dt, gc); err != nil {
146+
grafanaConfig := mcpgrafana.GrafanaConfig{Debug: gc.debug}
147+
148+
if err := run(transport, *addr, *basePath, *endpointPath, parseLevel(*logLevel), dt, grafanaConfig); err != nil {
147149
panic(err)
148150
}
149151
}

mcpgrafana.go

Lines changed: 60 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -33,33 +33,46 @@ func urlAndAPIKeyFromEnv() (string, string) {
3333
}
3434

3535
func urlAndAPIKeyFromHeaders(req *http.Request) (string, string) {
36-
u := req.Header.Get(grafanaURLHeader)
36+
u := strings.TrimRight(req.Header.Get(grafanaURLHeader), "/")
3737
apiKey := req.Header.Get(grafanaAPIKeyHeader)
3838
return u, apiKey
3939
}
4040

41-
type grafanaURLKey struct{}
42-
type grafanaAPIKeyKey struct{}
43-
type grafanaAccessTokenKey struct{}
41+
// grafanaConfigKey is the context key for Grafana configuration.
42+
type grafanaConfigKey struct{}
4443

45-
// grafanaDebugKey is the context key for the Grafana transport's debug flag.
46-
type grafanaDebugKey struct{}
44+
// GrafanaConfig represents the full configuration for Grafana clients.
45+
type GrafanaConfig struct {
46+
// Debug enables debug mode for the Grafana client.
47+
Debug bool
4748

48-
// WithGrafanaDebug adds the Grafana debug flag to the context.
49-
func WithGrafanaDebug(ctx context.Context, debug bool) context.Context {
50-
if debug {
51-
slog.Info("Grafana transport debug mode enabled")
52-
}
53-
return context.WithValue(ctx, grafanaDebugKey{}, debug)
49+
// URL is the URL of the Grafana instance.
50+
URL string
51+
52+
// APIKey is the API key or service account token for the Grafana instance.
53+
// It may be empty if we are using on-behalf-of auth.
54+
APIKey string
55+
56+
// AccessToken is the Grafana Cloud access policy token used for on-behalf-of auth in Grafana Cloud.
57+
AccessToken string
58+
// IDToken is an ID token identifying the user for the current request.
59+
// It comes from the `X-Grafana-Id` header sent from Grafana to plugin backends.
60+
// It is used for on-behalf-of auth in Grafana Cloud.
61+
IDToken string
62+
}
63+
64+
// WithGrafanaConfig adds Grafana configuration to the context.
65+
func WithGrafanaConfig(ctx context.Context, config GrafanaConfig) context.Context {
66+
return context.WithValue(ctx, grafanaConfigKey{}, config)
5467
}
5568

56-
// GrafanaDebugFromContext extracts the Grafana debug flag from the context.
57-
// If the flag is not set, it returns false.
58-
func GrafanaDebugFromContext(ctx context.Context) bool {
59-
if debug, ok := ctx.Value(grafanaDebugKey{}).(bool); ok {
60-
return debug
69+
// GrafanaConfigFromContext extracts Grafana configuration from the context.
70+
// If no config is found, returns a zero-value GrafanaConfig.
71+
func GrafanaConfigFromContext(ctx context.Context) GrafanaConfig {
72+
if config, ok := ctx.Value(grafanaConfigKey{}).(GrafanaConfig); ok {
73+
return config
6174
}
62-
return false
75+
return GrafanaConfig{}
6376
}
6477

6578
// ExtractGrafanaInfoFromEnv is a StdioContextFunc that extracts Grafana configuration
@@ -74,7 +87,13 @@ var ExtractGrafanaInfoFromEnv server.StdioContextFunc = func(ctx context.Context
7487
panic(fmt.Errorf("invalid Grafana URL %s: %w", u, err))
7588
}
7689
slog.Info("Using Grafana configuration", "url", parsedURL.Redacted(), "api_key_set", apiKey != "")
77-
return WithGrafanaURL(WithGrafanaAPIKey(ctx, apiKey), u)
90+
91+
// Get existing config or create a new one.
92+
// This will respect the existing debug flag, if set.
93+
config := GrafanaConfigFromContext(ctx)
94+
config.URL = u
95+
config.APIKey = apiKey
96+
return WithGrafanaConfig(ctx, config)
7897
}
7998

8099
// httpContextFunc is a function that can be used as a `server.HTTPContextFunc` or a
@@ -96,30 +115,29 @@ var ExtractGrafanaInfoFromHeaders httpContextFunc = func(ctx context.Context, re
96115
if apiKey == "" {
97116
apiKey = apiKeyEnv
98117
}
99-
return WithGrafanaURL(WithGrafanaAPIKey(ctx, apiKey), u)
100-
}
101118

102-
// WithGrafanaURL adds the Grafana URL to the context.
103-
func WithGrafanaURL(ctx context.Context, url string) context.Context {
104-
return context.WithValue(ctx, grafanaURLKey{}, url)
105-
}
106-
107-
// WithGrafanaAPIKey adds the Grafana API key to the context.
108-
func WithGrafanaAPIKey(ctx context.Context, apiKey string) context.Context {
109-
return context.WithValue(ctx, grafanaAPIKeyKey{}, apiKey)
119+
// Get existing config or create a new one.
120+
// This will respect the existing debug flag, if set.
121+
config := GrafanaConfigFromContext(ctx)
122+
config.URL = u
123+
config.APIKey = apiKey
124+
return WithGrafanaConfig(ctx, config)
110125
}
111126

112127
// WithOnBehalfOfAuth adds the Grafana access token and user token to the
113-
// context. These tokens are used for on-behalf-of auth in Grafana Cloud.
128+
// Grafana config. These tokens are used for on-behalf-of auth in Grafana Cloud.
114129
func WithOnBehalfOfAuth(ctx context.Context, accessToken, userToken string) (context.Context, error) {
115130
if accessToken == "" || userToken == "" {
116131
return nil, fmt.Errorf("neither accessToken nor userToken can be empty")
117132
}
118-
return context.WithValue(ctx, grafanaAccessTokenKey{}, []string{accessToken, userToken}), nil
133+
cfg := GrafanaConfigFromContext(ctx)
134+
cfg.AccessToken = accessToken
135+
cfg.IDToken = userToken
136+
return WithGrafanaConfig(ctx, cfg), nil
119137
}
120138

121139
// MustWithOnBehalfOfAuth adds the access and user tokens to the context,
122-
// panicing if either are empty.
140+
// panicking if either are empty.
123141
func MustWithOnBehalfOfAuth(ctx context.Context, accessToken, userToken string) context.Context {
124142
ctx, err := WithOnBehalfOfAuth(ctx, accessToken, userToken)
125143
if err != nil {
@@ -128,31 +146,6 @@ func MustWithOnBehalfOfAuth(ctx context.Context, accessToken, userToken string)
128146
return ctx
129147
}
130148

131-
// GrafanaURLFromContext extracts the Grafana URL from the context.
132-
func GrafanaURLFromContext(ctx context.Context) string {
133-
if u, ok := ctx.Value(grafanaURLKey{}).(string); ok {
134-
return u
135-
}
136-
return defaultGrafanaURL
137-
}
138-
139-
// GrafanaAPIKeyFromContext extracts the Grafana API key from the context.
140-
func GrafanaAPIKeyFromContext(ctx context.Context) string {
141-
if k, ok := ctx.Value(grafanaAPIKeyKey{}).(string); ok {
142-
return k
143-
}
144-
return ""
145-
}
146-
147-
// OnBehalfOfAuthFromContext extracts the Grafana access and user tokens from
148-
// the context. These tokens are used for on-behalf-of auth in Grafana Cloud.
149-
func OnBehalfOfAuthFromContext(ctx context.Context) (string, string) {
150-
if k, ok := ctx.Value(grafanaAccessTokenKey{}).([]string); ok {
151-
return k[0], k[1]
152-
}
153-
return "", ""
154-
}
155-
156149
type grafanaClientKey struct{}
157150

158151
func makeBasePath(path string) string {
@@ -188,7 +181,8 @@ func NewGrafanaClient(ctx context.Context, grafanaURL, apiKey string) *client.Gr
188181
cfg.APIKey = apiKey
189182
}
190183

191-
cfg.Debug = GrafanaDebugFromContext(ctx)
184+
config := GrafanaConfigFromContext(ctx)
185+
cfg.Debug = config.Debug
192186

193187
slog.Debug("Creating Grafana client", "url", parsedURL.Redacted(), "api_key_set", apiKey != "")
194188
return client.NewHTTPClientWithConfig(strfmt.Default, cfg)
@@ -258,6 +252,7 @@ var ExtractIncidentClientFromEnv server.StdioContextFunc = func(ctx context.Cont
258252
}
259253
slog.Debug("Creating Incident client", "url", parsedURL.Redacted(), "api_key_set", apiKey != "")
260254
client := incident.NewClient(incidentURL, apiKey)
255+
261256
return context.WithValue(ctx, incidentClientKey{}, client)
262257
}
263258

@@ -275,6 +270,7 @@ var ExtractIncidentClientFromHeaders httpContextFunc = func(ctx context.Context,
275270
}
276271
incidentURL := fmt.Sprintf("%s/api/plugins/grafana-irm-app/resources/api/v1/", grafanaURL)
277272
client := incident.NewClient(incidentURL, apiKey)
273+
278274
return context.WithValue(ctx, incidentClientKey{}, client)
279275
}
280276

@@ -322,10 +318,10 @@ func ComposeHTTPContextFuncs(funcs ...httpContextFunc) server.HTTPContextFunc {
322318

323319
// ComposedStdioContextFunc returns a StdioContextFunc that comprises all predefined StdioContextFuncs,
324320
// as well as the Grafana debug flag.
325-
func ComposedStdioContextFunc(debug bool) server.StdioContextFunc {
321+
func ComposedStdioContextFunc(config GrafanaConfig) server.StdioContextFunc {
326322
return ComposeStdioContextFuncs(
327323
func(ctx context.Context) context.Context {
328-
return WithGrafanaDebug(ctx, debug)
324+
return WithGrafanaConfig(ctx, config)
329325
},
330326
ExtractGrafanaInfoFromEnv,
331327
ExtractGrafanaClientFromEnv,
@@ -334,10 +330,10 @@ func ComposedStdioContextFunc(debug bool) server.StdioContextFunc {
334330
}
335331

336332
// ComposedSSEContextFunc is a SSEContextFunc that comprises all predefined SSEContextFuncs.
337-
func ComposedSSEContextFunc(debug bool) server.SSEContextFunc {
333+
func ComposedSSEContextFunc(config GrafanaConfig) server.SSEContextFunc {
338334
return ComposeSSEContextFuncs(
339335
func(ctx context.Context, req *http.Request) context.Context {
340-
return WithGrafanaDebug(ctx, debug)
336+
return WithGrafanaConfig(ctx, config)
341337
},
342338
ExtractGrafanaInfoFromHeaders,
343339
ExtractGrafanaClientFromHeaders,
@@ -346,10 +342,10 @@ func ComposedSSEContextFunc(debug bool) server.SSEContextFunc {
346342
}
347343

348344
// ComposedHTTPContextFunc is a HTTPContextFunc that comprises all predefined HTTPContextFuncs.
349-
func ComposedHTTPContextFunc(debug bool) server.HTTPContextFunc {
345+
func ComposedHTTPContextFunc(config GrafanaConfig) server.HTTPContextFunc {
350346
return ComposeHTTPContextFuncs(
351347
func(ctx context.Context, req *http.Request) context.Context {
352-
return WithGrafanaDebug(ctx, debug)
348+
return WithGrafanaConfig(ctx, config)
353349
},
354350
ExtractGrafanaInfoFromHeaders,
355351
ExtractGrafanaClientFromHeaders,

mcpgrafana_test.go

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,9 @@ func TestExtractGrafanaInfoFromHeaders(t *testing.T) {
7474
req, err := http.NewRequest("GET", "http://example.com", nil)
7575
require.NoError(t, err)
7676
ctx := ExtractGrafanaInfoFromHeaders(context.Background(), req)
77-
url := GrafanaURLFromContext(ctx)
78-
assert.Equal(t, defaultGrafanaURL, url)
79-
apiKey := GrafanaAPIKeyFromContext(ctx)
80-
assert.Equal(t, "", apiKey)
77+
config := GrafanaConfigFromContext(ctx)
78+
assert.Equal(t, defaultGrafanaURL, config.URL)
79+
assert.Equal(t, "", config.APIKey)
8180
})
8281

8382
t.Run("no headers, with env", func(t *testing.T) {
@@ -87,10 +86,9 @@ func TestExtractGrafanaInfoFromHeaders(t *testing.T) {
8786
req, err := http.NewRequest("GET", "http://example.com", nil)
8887
require.NoError(t, err)
8988
ctx := ExtractGrafanaInfoFromHeaders(context.Background(), req)
90-
url := GrafanaURLFromContext(ctx)
91-
assert.Equal(t, "http://my-test-url.grafana.com", url)
92-
apiKey := GrafanaAPIKeyFromContext(ctx)
93-
assert.Equal(t, "my-test-api-key", apiKey)
89+
config := GrafanaConfigFromContext(ctx)
90+
assert.Equal(t, "http://my-test-url.grafana.com", config.URL)
91+
assert.Equal(t, "my-test-api-key", config.APIKey)
9492
})
9593

9694
t.Run("with headers, no env", func(t *testing.T) {
@@ -99,10 +97,9 @@ func TestExtractGrafanaInfoFromHeaders(t *testing.T) {
9997
req.Header.Set(grafanaURLHeader, "http://my-test-url.grafana.com")
10098
req.Header.Set(grafanaAPIKeyHeader, "my-test-api-key")
10199
ctx := ExtractGrafanaInfoFromHeaders(context.Background(), req)
102-
url := GrafanaURLFromContext(ctx)
103-
assert.Equal(t, "http://my-test-url.grafana.com", url)
104-
apiKey := GrafanaAPIKeyFromContext(ctx)
105-
assert.Equal(t, "my-test-api-key", apiKey)
100+
config := GrafanaConfigFromContext(ctx)
101+
assert.Equal(t, "http://my-test-url.grafana.com", config.URL)
102+
assert.Equal(t, "my-test-api-key", config.APIKey)
106103
})
107104

108105
t.Run("with headers, with env", func(t *testing.T) {
@@ -115,10 +112,9 @@ func TestExtractGrafanaInfoFromHeaders(t *testing.T) {
115112
req.Header.Set(grafanaURLHeader, "http://my-test-url.grafana.com")
116113
req.Header.Set(grafanaAPIKeyHeader, "my-test-api-key")
117114
ctx := ExtractGrafanaInfoFromHeaders(context.Background(), req)
118-
url := GrafanaURLFromContext(ctx)
119-
assert.Equal(t, "http://my-test-url.grafana.com", url)
120-
apiKey := GrafanaAPIKeyFromContext(ctx)
121-
assert.Equal(t, "my-test-api-key", apiKey)
115+
config := GrafanaConfigFromContext(ctx)
116+
assert.Equal(t, "http://my-test-url.grafana.com", config.URL)
117+
assert.Equal(t, "my-test-api-key", config.APIKey)
122118
})
123119
}
124120

tools/alerting_client.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,24 @@ const (
2323
type alertingClient struct {
2424
baseURL *url.URL
2525
accessToken string
26-
userToken string
26+
idToken string
2727
apiKey string
2828
httpClient *http.Client
2929
}
3030

3131
func newAlertingClientFromContext(ctx context.Context) (*alertingClient, error) {
32-
baseURL := strings.TrimRight(mcpgrafana.GrafanaURLFromContext(ctx), "/")
32+
cfg := mcpgrafana.GrafanaConfigFromContext(ctx)
33+
baseURL := strings.TrimRight(cfg.URL, "/")
3334
parsedBaseURL, err := url.Parse(baseURL)
3435
if err != nil {
3536
return nil, fmt.Errorf("invalid Grafana base URL %q: %w", baseURL, err)
3637
}
37-
accessToken, userToken := mcpgrafana.OnBehalfOfAuthFromContext(ctx)
3838

3939
return &alertingClient{
4040
baseURL: parsedBaseURL,
41-
accessToken: accessToken,
42-
userToken: userToken,
43-
apiKey: mcpgrafana.GrafanaAPIKeyFromContext(ctx),
41+
accessToken: cfg.AccessToken,
42+
idToken: cfg.IDToken,
43+
apiKey: cfg.APIKey,
4444
httpClient: &http.Client{
4545
Timeout: defaultTimeout,
4646
},
@@ -59,9 +59,9 @@ func (c *alertingClient) makeRequest(ctx context.Context, path string) (*http.Re
5959
req.Header.Set("Content-Type", "application/json")
6060

6161
// If accessToken is set we use that first and fall back to normal Authorization.
62-
if c.accessToken != "" && c.userToken != "" {
62+
if c.accessToken != "" && c.idToken != "" {
6363
req.Header.Set("X-Access-Token", c.accessToken)
64-
req.Header.Set("X-Grafana-Id", c.userToken)
64+
req.Header.Set("X-Grafana-Id", c.idToken)
6565
} else if c.apiKey != "" {
6666
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
6767
}

tools/alerting_client_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,11 @@ func TestAlertingClient_GetRules_Error(t *testing.T) {
102102
}
103103

104104
func TestNewAlertingClientFromContext(t *testing.T) {
105-
ctx := mcpgrafana.WithGrafanaURL(context.Background(), "http://localhost:3000/")
106-
ctx = mcpgrafana.WithGrafanaAPIKey(ctx, "test-api-key")
105+
config := mcpgrafana.GrafanaConfig{
106+
URL: "http://localhost:3000/",
107+
APIKey: "test-api-key",
108+
}
109+
ctx := mcpgrafana.WithGrafanaConfig(context.Background(), config)
107110

108111
client, err := newAlertingClientFromContext(ctx)
109112
require.NoError(t, err)

0 commit comments

Comments
 (0)