Skip to content

Commit c53e4a3

Browse files
committed
Add initial Go scaffolding
1 parent c5822b8 commit c53e4a3

File tree

5 files changed

+307
-0
lines changed

5 files changed

+307
-0
lines changed

client.go

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package mcpgrafana
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"net/url"
8+
"os"
9+
10+
"github.com/go-openapi/strfmt"
11+
"github.com/grafana/grafana-openapi-client-go/client"
12+
"github.com/mark3labs/mcp-go/server"
13+
)
14+
15+
type clientKey struct{}
16+
17+
const (
18+
grafanaURLEnvVar = "GRAFANA_URL"
19+
grafanaAPIEnvVar = "GRAFANA_API_KEY"
20+
21+
grafanaURLHeader = "X-Grafana-URL"
22+
grafanaAPIKeyHeader = "X-Grafana-API-Key"
23+
)
24+
25+
// ExtractClientFromEnv is a StdioContextFunc that extracts Grafana configuration
26+
// from environment variables and injects a configured client into the context.
27+
var ExtractClientFromEnv server.StdioContextFunc = func(ctx context.Context) context.Context {
28+
cfg := client.DefaultTransportConfig()
29+
// Extract transport config from env vars, and set it on the context.
30+
if u, ok := os.LookupEnv(grafanaURLEnvVar); ok {
31+
url, err := url.Parse(u)
32+
if err != nil {
33+
panic(fmt.Errorf("invalid %s: %w", grafanaURLEnvVar, err))
34+
}
35+
cfg.Host = url.Host
36+
// The Grafana client will always prefer HTTPS even if the URL is HTTP,
37+
// so we need to limit the schemes to HTTP if the URL is HTTP.
38+
if url.Scheme == "http" {
39+
cfg.Schemes = []string{"http"}
40+
}
41+
}
42+
if apiKey := os.Getenv(grafanaAPIEnvVar); apiKey != "" {
43+
cfg.APIKey = apiKey
44+
}
45+
46+
client := client.NewHTTPClientWithConfig(strfmt.Default, cfg)
47+
return context.WithValue(ctx, clientKey{}, client)
48+
}
49+
50+
// ExtractClientFromHeaders is a SSEContextFunc that extracts Grafana configuration
51+
// from request headers and injects a configured client into the context.
52+
var ExtractClientFromHeaders server.SSEContextFunc = func(ctx context.Context, req *http.Request) context.Context {
53+
cfg := client.DefaultTransportConfig()
54+
// Extract transport config from request headers, and set it on the context.
55+
if u := req.Header.Get(grafanaURLHeader); u != "" {
56+
if url, err := url.Parse(u); err == nil {
57+
cfg.Host = url.Host
58+
if url.Scheme == "http" {
59+
cfg.Schemes = []string{"http"}
60+
}
61+
}
62+
}
63+
if apiKey := req.Header.Get(grafanaAPIKeyHeader); apiKey != "" {
64+
cfg.APIKey = apiKey
65+
}
66+
client := client.NewHTTPClientWithConfig(strfmt.Default, cfg)
67+
return WithGrafanaClient(ctx, client)
68+
}
69+
70+
// WithGrafanaClient sets the Grafana client in the context.
71+
//
72+
// It can be retrieved using GrafanaClientFromContext.
73+
func WithGrafanaClient(ctx context.Context, client *client.GrafanaHTTPAPI) context.Context {
74+
return context.WithValue(ctx, clientKey{}, client)
75+
}
76+
77+
// GrafanaClientFromContext retrieves the Grafana client from the context.
78+
func GrafanaClientFromContext(ctx context.Context) *client.GrafanaHTTPAPI {
79+
c, ok := ctx.Value(clientKey{}).(*client.GrafanaHTTPAPI)
80+
if !ok {
81+
return nil
82+
}
83+
return c
84+
}

cmd/main.go

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"flag"
6+
"fmt"
7+
"log"
8+
"os"
9+
10+
"github.com/mark3labs/mcp-go/server"
11+
12+
mcpgrafana "github.com/grafana/mcp-grafana"
13+
"github.com/grafana/mcp-grafana/tools"
14+
)
15+
16+
func newServer() *server.MCPServer {
17+
s := server.NewMCPServer(
18+
"mcp-grafana",
19+
"0.1.0",
20+
)
21+
s.AddTool(tools.SearchDashboardsTool, tools.SearchDashboardsHandler)
22+
return s
23+
}
24+
25+
func run(transport string) error {
26+
s := newServer()
27+
28+
switch transport {
29+
case "stdio":
30+
srv := server.NewStdioServer(s)
31+
srv.SetContextFunc(mcpgrafana.ExtractClientFromEnv)
32+
return srv.Listen(context.Background(), os.Stdin, os.Stdout)
33+
case "sse":
34+
addr := "http://localhost:8080"
35+
srv := server.NewSSEServer(s, addr)
36+
srv.SetContextFunc(mcpgrafana.ExtractClientFromHeaders)
37+
log.Printf("SSE server listening on %s", addr)
38+
if err := srv.Start("localhost:8080"); err != nil {
39+
return fmt.Errorf("Server error: %v", err)
40+
}
41+
default:
42+
return fmt.Errorf(
43+
"Invalid transport type: %s. Must be 'stdio' or 'sse'",
44+
transport,
45+
)
46+
}
47+
return nil
48+
}
49+
50+
func main() {
51+
var transport string
52+
flag.StringVar(&transport, "t", "stdio", "Transport type (stdio or sse)")
53+
flag.StringVar(
54+
&transport,
55+
"transport",
56+
"stdio",
57+
"Transport type (stdio or sse)",
58+
)
59+
flag.Parse()
60+
61+
if err := run(transport); err != nil {
62+
panic(err)
63+
}
64+
}

go.mod

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
module github.com/grafana/mcp-grafana
2+
3+
go 1.24.0
4+
5+
require (
6+
github.com/go-openapi/strfmt v0.23.0
7+
github.com/grafana/grafana-openapi-client-go v0.0.0-20250108132429-8d7e1f158f65
8+
github.com/mark3labs/mcp-go v0.8.5
9+
)
10+
11+
require (
12+
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
13+
github.com/go-logr/logr v1.4.1 // indirect
14+
github.com/go-logr/stdr v1.2.2 // indirect
15+
github.com/go-openapi/analysis v0.23.0 // indirect
16+
github.com/go-openapi/errors v0.22.0 // indirect
17+
github.com/go-openapi/jsonpointer v0.21.0 // indirect
18+
github.com/go-openapi/jsonreference v0.21.0 // indirect
19+
github.com/go-openapi/loads v0.22.0 // indirect
20+
github.com/go-openapi/runtime v0.28.0 // indirect
21+
github.com/go-openapi/spec v0.21.0 // indirect
22+
github.com/go-openapi/swag v0.23.0 // indirect
23+
github.com/go-openapi/validate v0.24.0 // indirect
24+
github.com/google/uuid v1.6.0 // indirect
25+
github.com/josharian/intern v1.0.0 // indirect
26+
github.com/mailru/easyjson v0.7.7 // indirect
27+
github.com/mitchellh/mapstructure v1.5.0 // indirect
28+
github.com/oklog/ulid v1.3.1 // indirect
29+
github.com/opentracing/opentracing-go v1.2.0 // indirect
30+
go.mongodb.org/mongo-driver v1.14.0 // indirect
31+
go.opentelemetry.io/otel v1.24.0 // indirect
32+
go.opentelemetry.io/otel/metric v1.24.0 // indirect
33+
go.opentelemetry.io/otel/trace v1.24.0 // indirect
34+
golang.org/x/sync v0.6.0 // indirect
35+
gopkg.in/yaml.v3 v3.0.1 // indirect
36+
)
37+
38+
replace github.com/mark3labs/mcp-go => github.com/grafana/mcp-go v0.8.6-0.20250226194234-5e1fa6f6f8af

go.sum

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
2+
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
3+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
5+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6+
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
7+
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
8+
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
9+
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
10+
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
11+
github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=
12+
github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=
13+
github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w=
14+
github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE=
15+
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
16+
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
17+
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
18+
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
19+
github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco=
20+
github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs=
21+
github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ=
22+
github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc=
23+
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
24+
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
25+
github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=
26+
github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
27+
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
28+
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
29+
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
30+
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
31+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
32+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
33+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
34+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
35+
github.com/grafana/grafana-openapi-client-go v0.0.0-20250108132429-8d7e1f158f65 h1:AnfwjPE8TXJO8CX0Q5PvtzGta9Ls3iRASWVV4jHl4KA=
36+
github.com/grafana/grafana-openapi-client-go v0.0.0-20250108132429-8d7e1f158f65/go.mod h1:hiZnMmXc9KXNUlvkV2BKFsiWuIFF/fF4wGgYWEjBitI=
37+
github.com/grafana/mcp-go v0.8.6-0.20250226194234-5e1fa6f6f8af h1:SwCTIu0dpOapmOc+3HQtC5VM7CUimW0r83u+5fCgQTU=
38+
github.com/grafana/mcp-go v0.8.6-0.20250226194234-5e1fa6f6f8af/go.mod h1:cjMlBU0cv/cj9kjlgmRhoJ5JREdS7YX83xeIG9Ko/jE=
39+
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
40+
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
41+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
42+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
43+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
44+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
45+
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
46+
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
47+
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
48+
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
49+
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
50+
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
51+
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
52+
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
53+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
54+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
55+
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
56+
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
57+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
58+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
59+
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
60+
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
61+
go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
62+
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
63+
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
64+
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
65+
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
66+
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
67+
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
68+
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
69+
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
70+
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
71+
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
72+
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
73+
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
74+
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
75+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
76+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
77+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
78+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
79+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

tools/search.go

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package tools
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
8+
"github.com/mark3labs/mcp-go/mcp"
9+
10+
"github.com/grafana/grafana-openapi-client-go/client/search"
11+
mcpgrafana "github.com/grafana/mcp-grafana"
12+
)
13+
14+
type SearchDashboardsParams struct {
15+
}
16+
17+
func SearchDashboardsHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
18+
c := mcpgrafana.GrafanaClientFromContext(ctx)
19+
params := search.NewSearchParamsWithContext(ctx)
20+
if q, ok := request.Params.Arguments["query"]; ok {
21+
if q, ok := q.(string); ok {
22+
params.SetQuery(&q)
23+
}
24+
}
25+
search, err := c.Search.Search(params)
26+
if err != nil {
27+
return nil, fmt.Errorf("search dashboards for %+v: %w", c, err)
28+
}
29+
b, err := json.Marshal(search.Payload)
30+
if err != nil {
31+
return nil, fmt.Errorf("marshal search results: %w", err)
32+
}
33+
return mcp.NewToolResultText(string(b)), nil
34+
}
35+
36+
var SearchDashboardsTool = mcp.NewTool("search_dashboards",
37+
mcp.WithDescription("Search for dashboards"),
38+
mcp.WithString("query",
39+
mcp.Description("Query string"),
40+
mcp.Required(),
41+
),
42+
)

0 commit comments

Comments
 (0)