@@ -10,7 +10,9 @@ import (
10
10
"net/url"
11
11
"os"
12
12
"reflect"
13
+ "runtime/debug"
13
14
"strings"
15
+ "sync"
14
16
15
17
"github.com/go-openapi/strfmt"
16
18
"github.com/grafana/grafana-openapi-client-go/client"
@@ -59,7 +61,7 @@ type GrafanaConfig struct {
59
61
Debug bool
60
62
61
63
// IncludeArgumentsInSpans enables logging of tool arguments in OpenTelemetry spans.
62
- // This should only be enabled in non-production environments or when you're certain
64
+ // This should only be enabled in non-production environments or when you're certain
63
65
// the arguments don't contain PII. Defaults to false for safety.
64
66
// Note: OpenTelemetry spans are always created for context propagation, but arguments
65
67
// are only included when this flag is enabled.
@@ -147,6 +149,65 @@ func (tc *TLSConfig) HTTPTransport(defaultTransport *http.Transport) (http.Round
147
149
return transport , nil
148
150
}
149
151
152
+ // UserAgentTransport wraps an http.RoundTripper to add a custom User-Agent header
153
+ type UserAgentTransport struct {
154
+ rt http.RoundTripper
155
+ UserAgent string
156
+ }
157
+
158
+ func (t * UserAgentTransport ) RoundTrip (req * http.Request ) (* http.Response , error ) {
159
+ // Clone the request to avoid modifying the original
160
+ clonedReq := req .Clone (req .Context ())
161
+
162
+ // Add or update the User-Agent header
163
+ if clonedReq .Header .Get ("User-Agent" ) == "" {
164
+ clonedReq .Header .Set ("User-Agent" , t .UserAgent )
165
+ }
166
+
167
+ return t .rt .RoundTrip (clonedReq )
168
+ }
169
+
170
+ // Version returns the version of the mcp-grafana binary.
171
+ // It is populated by the `runtime/debug` package which
172
+ // fetches git information from the build directory.
173
+ var Version = sync .OnceValue (func () string {
174
+ // Default version string returned by `runtime/debug` if built
175
+ // from the source repository rather than with `go install`.
176
+ v := "(devel)"
177
+ if bi , ok := debug .ReadBuildInfo (); ok && bi .Main .Version != "" {
178
+ v = bi .Main .Version
179
+ }
180
+ return v
181
+ })
182
+
183
+ // UserAgent returns the user agent string for HTTP requests
184
+ func UserAgent () string {
185
+ return fmt .Sprintf ("mcp-grafana/%s" , Version ())
186
+ }
187
+
188
+ // NewUserAgentTransport creates a new UserAgentTransport with the specified user agent.
189
+ // If no user agent is provided, uses the default UserAgent().
190
+ func NewUserAgentTransport (rt http.RoundTripper , userAgent ... string ) * UserAgentTransport {
191
+ if rt == nil {
192
+ rt = http .DefaultTransport
193
+ }
194
+
195
+ ua := UserAgent () // default
196
+ if len (userAgent ) > 0 {
197
+ ua = userAgent [0 ]
198
+ }
199
+
200
+ return & UserAgentTransport {
201
+ rt : rt ,
202
+ UserAgent : ua ,
203
+ }
204
+ }
205
+
206
+ // wrapWithUserAgent wraps an http.RoundTripper with user agent tracking
207
+ func wrapWithUserAgent (rt http.RoundTripper ) http.RoundTripper {
208
+ return NewUserAgentTransport (rt )
209
+ }
210
+
150
211
// ExtractGrafanaInfoFromEnv is a StdioContextFunc that extracts Grafana configuration
151
212
// from environment variables and injects a configured client into the context.
152
213
var ExtractGrafanaInfoFromEnv server.StdioContextFunc = func (ctx context.Context ) context.Context {
@@ -281,9 +342,11 @@ func NewGrafanaClient(ctx context.Context, grafanaURL, apiKey string) *client.Gr
281
342
transportField := v .FieldByName ("Transport" )
282
343
if transportField .IsValid () && transportField .CanSet () {
283
344
if rt , ok := transportField .Interface ().(http.RoundTripper ); ok {
284
- wrapped := otelhttp .NewTransport (rt )
345
+ // Wrap with user agent first, then otel
346
+ userAgentWrapped := wrapWithUserAgent (rt )
347
+ wrapped := otelhttp .NewTransport (userAgentWrapped )
285
348
transportField .Set (reflect .ValueOf (wrapped ))
286
- slog .Debug ("HTTP tracing enabled for Grafana client" )
349
+ slog .Debug ("HTTP tracing and user agent tracking enabled for Grafana client" )
287
350
}
288
351
}
289
352
}
@@ -363,12 +426,15 @@ var ExtractIncidentClientFromEnv server.StdioContextFunc = func(ctx context.Cont
363
426
if err != nil {
364
427
slog .Error ("Failed to create custom transport for incident client, using default" , "error" , err )
365
428
} else {
366
- client .HTTPClient .Transport = transport
367
- slog .Debug ("Using custom TLS configuration for incident client" ,
429
+ client .HTTPClient .Transport = wrapWithUserAgent ( transport )
430
+ slog .Debug ("Using custom TLS configuration and user agent for incident client" ,
368
431
"cert_file" , tlsConfig .CertFile ,
369
432
"ca_file" , tlsConfig .CAFile ,
370
433
"skip_verify" , tlsConfig .SkipVerify )
371
434
}
435
+ } else {
436
+ // No custom TLS, but still add user agent
437
+ client .HTTPClient .Transport = wrapWithUserAgent (http .DefaultTransport )
372
438
}
373
439
374
440
return context .WithValue (ctx , incidentClientKey {}, client )
@@ -395,12 +461,15 @@ var ExtractIncidentClientFromHeaders httpContextFunc = func(ctx context.Context,
395
461
if err != nil {
396
462
slog .Error ("Failed to create custom transport for incident client, using default" , "error" , err )
397
463
} else {
398
- client .HTTPClient .Transport = transport
399
- slog .Debug ("Using custom TLS configuration for incident client" ,
464
+ client .HTTPClient .Transport = wrapWithUserAgent ( transport )
465
+ slog .Debug ("Using custom TLS configuration and user agent for incident client" ,
400
466
"cert_file" , tlsConfig .CertFile ,
401
467
"ca_file" , tlsConfig .CAFile ,
402
468
"skip_verify" , tlsConfig .SkipVerify )
403
469
}
470
+ } else {
471
+ // No custom TLS, but still add user agent
472
+ client .HTTPClient .Transport = wrapWithUserAgent (http .DefaultTransport )
404
473
}
405
474
406
475
return context .WithValue (ctx , incidentClientKey {}, client )
0 commit comments