Skip to content

Commit 317a563

Browse files
committed
Add Incident tools
1 parent 342cbf7 commit 317a563

File tree

5 files changed

+177
-2
lines changed

5 files changed

+177
-2
lines changed

client.go

+44-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/go-openapi/strfmt"
1111
"github.com/grafana/grafana-openapi-client-go/client"
12+
"github.com/grafana/incident-go"
1213
"github.com/mark3labs/mcp-go/server"
1314
)
1415

@@ -22,6 +23,18 @@ const (
2223
grafanaAPIKeyHeader = "X-Grafana-API-Key"
2324
)
2425

26+
func urlAndAPIKeyFromEnv() (string, string) {
27+
u := os.Getenv(grafanaURLEnvVar)
28+
apiKey := os.Getenv(grafanaAPIEnvVar)
29+
return u, apiKey
30+
}
31+
32+
func urlAndAPIKeyFromHeaders(req *http.Request) (string, string) {
33+
u := req.Header.Get(grafanaURLHeader)
34+
apiKey := req.Header.Get(grafanaAPIKeyHeader)
35+
return u, apiKey
36+
}
37+
2538
// ExtractClientFromEnv is a StdioContextFunc that extracts Grafana configuration
2639
// from environment variables and injects a configured client into the context.
2740
var ExtractClientFromEnv server.StdioContextFunc = func(ctx context.Context) context.Context {
@@ -52,15 +65,16 @@ var ExtractClientFromEnv server.StdioContextFunc = func(ctx context.Context) con
5265
var ExtractClientFromHeaders server.SSEContextFunc = func(ctx context.Context, req *http.Request) context.Context {
5366
cfg := client.DefaultTransportConfig()
5467
// Extract transport config from request headers, and set it on the context.
55-
if u := req.Header.Get(grafanaURLHeader); u != "" {
68+
u, apiKey := urlAndAPIKeyFromHeaders(req)
69+
if u != "" {
5670
if url, err := url.Parse(u); err == nil {
5771
cfg.Host = url.Host
5872
if url.Scheme == "http" {
5973
cfg.Schemes = []string{"http"}
6074
}
6175
}
6276
}
63-
if apiKey := req.Header.Get(grafanaAPIKeyHeader); apiKey != "" {
77+
if apiKey != "" {
6478
cfg.APIKey = apiKey
6579
}
6680
client := client.NewHTTPClientWithConfig(strfmt.Default, cfg)
@@ -82,3 +96,31 @@ func GrafanaClientFromContext(ctx context.Context) *client.GrafanaHTTPAPI {
8296
}
8397
return c
8498
}
99+
100+
type incidentClientKey struct{}
101+
102+
var ExtractIncidentClientFromEnv server.StdioContextFunc = func(ctx context.Context) context.Context {
103+
grafanaUrl, apiKey := urlAndAPIKeyFromEnv()
104+
incidentUrl := fmt.Sprintf("%s/api/plugins/grafana-incident-app/resources/api", grafanaUrl)
105+
client := incident.NewClient(incidentUrl, apiKey)
106+
return context.WithValue(ctx, incidentClientKey{}, client)
107+
}
108+
109+
var ExtractIncidentClientFromHeaders server.SSEContextFunc = func(ctx context.Context, req *http.Request) context.Context {
110+
grafanaUrl, apiKey := urlAndAPIKeyFromHeaders(req)
111+
incidentUrl := fmt.Sprintf("%s/api/plugins/grafana-incident-app/resources/api", grafanaUrl)
112+
client := incident.NewClient(incidentUrl, apiKey)
113+
return context.WithValue(ctx, incidentClientKey{}, client)
114+
}
115+
116+
func WithIncidentClient(ctx context.Context, client *incident.Client) context.Context {
117+
return context.WithValue(ctx, incidentClientKey{}, client)
118+
}
119+
120+
func IncidentClientFromContext(ctx context.Context) *incident.Client {
121+
c, ok := ctx.Value(incidentClientKey{}).(*incident.Client)
122+
if !ok {
123+
return nil
124+
}
125+
return c
126+
}

cmd/main.go

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ func newServer() *server.MCPServer {
2020
)
2121
tools.AddSearchTools(s)
2222
tools.AddDatasourceTools(s)
23+
tools.AddIncidentTools(s)
2324
return s
2425
}
2526

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ require (
2727
github.com/go-openapi/swag v0.23.0 // indirect
2828
github.com/go-openapi/validate v0.24.0 // indirect
2929
github.com/google/uuid v1.6.0 // indirect
30+
github.com/grafana/incident-go v0.0.0-20250211094540-dc6a98fdae43 // indirect
3031
github.com/josharian/intern v1.0.0 // indirect
3132
github.com/mailru/easyjson v0.7.7 // indirect
3233
github.com/mitchellh/mapstructure v1.5.0 // indirect

go.sum

+3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
3838
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
3939
github.com/grafana/grafana-openapi-client-go v0.0.0-20250108132429-8d7e1f158f65 h1:AnfwjPE8TXJO8CX0Q5PvtzGta9Ls3iRASWVV4jHl4KA=
4040
github.com/grafana/grafana-openapi-client-go v0.0.0-20250108132429-8d7e1f158f65/go.mod h1:hiZnMmXc9KXNUlvkV2BKFsiWuIFF/fF4wGgYWEjBitI=
41+
github.com/grafana/incident-go v0.0.0-20250211094540-dc6a98fdae43 h1:+MCsOKi5BJ1wO3PRj3eDNxCScYwE2IcKNW1t8OcTarE=
42+
github.com/grafana/incident-go v0.0.0-20250211094540-dc6a98fdae43/go.mod h1:3QDfdZOWKRxNhMJFL+0C/+12+jLNHDlt0VKNr/i9Daw=
4143
github.com/grafana/mcp-go v0.8.6-0.20250226194234-5e1fa6f6f8af h1:SwCTIu0dpOapmOc+3HQtC5VM7CUimW0r83u+5fCgQTU=
4244
github.com/grafana/mcp-go v0.8.6-0.20250226194234-5e1fa6f6f8af/go.mod h1:cjMlBU0cv/cj9kjlgmRhoJ5JREdS7YX83xeIG9Ko/jE=
4345
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
@@ -50,6 +52,7 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
5052
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
5153
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
5254
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
55+
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
5356
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
5457
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
5558
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=

tools/incident.go

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package tools
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
8+
"github.com/grafana/incident-go"
9+
mcpgrafana "github.com/grafana/mcp-grafana"
10+
"github.com/mark3labs/mcp-go/mcp"
11+
"github.com/mark3labs/mcp-go/server"
12+
)
13+
14+
type ListIncidentsParams struct {
15+
Limit int `json:"limit" jsonschema:"description=The maximum number of incidents to return"`
16+
Drill bool `json:"drill" jsonschema:"description=Whether to include drill incidents"`
17+
Status string `json:"status" jsonschema:"description=The status of the incidents to include"`
18+
}
19+
20+
func ListIncidents(ctx context.Context, args ListIncidentsParams) (*mcp.CallToolResult, error) {
21+
c := mcpgrafana.IncidentClientFromContext(ctx)
22+
is := incident.NewIncidentsService(c)
23+
query := ""
24+
if !args.Drill {
25+
query = "isdrill:false"
26+
}
27+
if args.Status != "" {
28+
query += fmt.Sprintf(" and status:%s", args.Status)
29+
}
30+
incidents, err := is.QueryIncidents(ctx, incident.QueryIncidentsRequest{
31+
Query: incident.IncidentsQuery{
32+
QueryString: query,
33+
OrderDirection: "DESC",
34+
Limit: args.Limit,
35+
},
36+
})
37+
if err != nil {
38+
return nil, fmt.Errorf("list incidents: %w", err)
39+
}
40+
b, err := json.Marshal(incidents.Incidents)
41+
if err != nil {
42+
return nil, fmt.Errorf("marshal incidents: %w", err)
43+
}
44+
return mcp.NewToolResultText(string(b)), nil
45+
}
46+
47+
var ListIncidentsTool, ListIncidentsHandler = mcpgrafana.MustTool(
48+
"list_incidents",
49+
"List incidents",
50+
ListIncidents,
51+
)
52+
53+
type CreateIncidentParams struct {
54+
Title string `json:"title" jsonschema:"description=The title of the incident"`
55+
Severity string `json:"severity" jsonschema:"description=The severity of the incident"`
56+
RoomPrefix string `json:"roomPrefix" jsonschema:"description=The prefix of the room to create the incident in"`
57+
IsDrill bool `json:"isDrill" jsonschema:"description=Whether the incident is a drill incident"`
58+
Status string `json:"status" jsonschema:"description=The status of the incident"`
59+
AttachCaption string `json:"attachCaption" jsonschema:"description=The caption of the attachment"`
60+
AttachURL string `json:"attachUrl" jsonschema:"description=The URL of the attachment"`
61+
Labels []incident.IncidentLabel `json:"labels" jsonschema:"description=The labels to add to the incident"`
62+
}
63+
64+
func CreateIncident(ctx context.Context, args CreateIncidentParams) (*mcp.CallToolResult, error) {
65+
c := mcpgrafana.IncidentClientFromContext(ctx)
66+
is := incident.NewIncidentsService(c)
67+
incident, err := is.CreateIncident(ctx, incident.CreateIncidentRequest{
68+
Title: args.Title,
69+
Severity: args.Severity,
70+
RoomPrefix: args.RoomPrefix,
71+
IsDrill: args.IsDrill,
72+
Status: args.Status,
73+
AttachCaption: args.AttachCaption,
74+
AttachURL: args.AttachURL,
75+
Labels: args.Labels,
76+
})
77+
if err != nil {
78+
return nil, fmt.Errorf("create incident: %w", err)
79+
}
80+
b, err := json.Marshal(incident)
81+
if err != nil {
82+
return nil, fmt.Errorf("marshal incident: %w", err)
83+
}
84+
return mcp.NewToolResultText(string(b)), nil
85+
}
86+
87+
var CreateIncidentTool, CreateIncidentHandler = mcpgrafana.MustTool(
88+
"create_incident",
89+
"Create an incident",
90+
CreateIncident,
91+
)
92+
93+
type AddActivityToIncidentParams struct {
94+
IncidentID string `json:"incidentId" jsonschema:"description=The ID of the incident to add the activity to"`
95+
Body string `json:"body" jsonschema:"description=The body of the activity. URLs will be parsed and attached as context"`
96+
EventTime string `json:"eventTime" jsonschema:"description=The time that the activity occurred. If not provided, the current time will be used"`
97+
}
98+
99+
func AddActivityToIncident(ctx context.Context, args AddActivityToIncidentParams) (*mcp.CallToolResult, error) {
100+
c := mcpgrafana.IncidentClientFromContext(ctx)
101+
as := incident.NewActivityService(c)
102+
activity, err := as.AddActivity(ctx, incident.AddActivityRequest{
103+
IncidentID: args.IncidentID,
104+
ActivityKind: "userNote",
105+
Body: args.Body,
106+
EventTime: args.EventTime,
107+
})
108+
if err != nil {
109+
return nil, fmt.Errorf("add activity to incident: %w", err)
110+
}
111+
b, err := json.Marshal(activity)
112+
if err != nil {
113+
return nil, fmt.Errorf("marshal incident: %w", err)
114+
}
115+
return mcp.NewToolResultText(string(b)), nil
116+
}
117+
118+
var AddActivityToIncidentTool, AddActivityToIncidentHandler = mcpgrafana.MustTool(
119+
"add_activity_to_incident",
120+
"Add an activity to an incident",
121+
AddActivityToIncident,
122+
)
123+
124+
func AddIncidentTools(mcp *server.MCPServer) {
125+
mcp.AddTool(ListIncidentsTool, ListIncidentsHandler)
126+
mcp.AddTool(CreateIncidentTool, CreateIncidentHandler)
127+
mcp.AddTool(AddActivityToIncidentTool, AddActivityToIncidentHandler)
128+
}

0 commit comments

Comments
 (0)