Skip to content

Commit 17be383

Browse files
Add alerting_manage_routing tool (#618)
Replace list_contact_points with alerting_manage_routing, a unified routing tool with five operations: - get_notification_policies: retrieve the notification policy tree - get_contact_points: list contact points (replaces list_contact_points) - get_contact_point: get a single contact point by title - get_time_intervals: list all time intervals - get_time_interval: get a single time interval by name
1 parent 1d40a71 commit 17be383

File tree

8 files changed

+748
-166
lines changed

8 files changed

+748
-166
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ The dashboard tools now include several strategies to manage context window usag
132132
- **List and fetch alert rule information:** View alert rules and their statuses (firing/normal/error/etc.) in Grafana. Supports both Grafana-managed rules and datasource-managed rules from Prometheus or Loki datasources.
133133
- **Create and update alert rules:** Create new alert rules or modify existing ones.
134134
- **Delete alert rules:** Remove alert rules by UID.
135-
- **List contact points:** View configured notification contact points in Grafana. Supports both Grafana-managed contact points and receivers from external Alertmanager datasources (Prometheus Alertmanager, Mimir, Cortex).
135+
- **Manage alerting routing:** View notification policies, contact points, and time intervals. Supports both Grafana-managed contact points and receivers from external Alertmanager datasources (Prometheus Alertmanager, Mimir, Cortex).
136136

137137
### Grafana OnCall
138138

@@ -294,7 +294,7 @@ Scopes define the specific resources that permissions apply to. Each action requ
294294
| `create_alert_rule` | Alerting | Create a new alert rule | `alert.rules:write` | `folders:*` or `folders:uid:alerts-folder` |
295295
| `update_alert_rule` | Alerting | Update an existing alert rule | `alert.rules:write` | `folders:uid:alerts-folder` |
296296
| `delete_alert_rule` | Alerting | Delete an alert rule by UID | `alert.rules:write` | `folders:uid:alerts-folder` |
297-
| `list_contact_points` | Alerting | List notification contact points (Grafana-managed and Alertmanager) | `alert.notifications:read` | Global scope |
297+
| `alerting_manage_routing` | Alerting | Manage notification policies, contact points, and time intervals | `alert.notifications:read` | Global scope |
298298
| `list_oncall_schedules` | OnCall | List schedules from Grafana OnCall | `grafana-oncall-app.schedules:read` | Plugin-specific scopes |
299299
| `get_oncall_shift` | OnCall | Get details for a specific OnCall shift | `grafana-oncall-app.schedules:read` | Plugin-specific scopes |
300300
| `get_current_oncall_users` | OnCall | Get users currently on-call for a specific schedule | `grafana-oncall-app.schedules:read` | Plugin-specific scopes |
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
apiVersion: 1
2+
3+
muteTimes:
4+
- orgId: 1
5+
name: weekends
6+
time_intervals:
7+
- weekdays: [saturday, sunday]
8+
9+
policies:
10+
- orgId: 1
11+
receiver: grafana-default-email
12+
group_by:
13+
- grafana_folder
14+
- alertname
15+
routes:
16+
- receiver: Email1
17+
object_matchers:
18+
- - severity
19+
- =
20+
- info
21+
mute_time_intervals:
22+
- weekends

tests/disable_write_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ async def test_disable_write_flag_disables_write_tools(grafana_env):
5555
"get_dashboard_by_uid",
5656
"list_alert_rules",
5757
"get_alert_rule_by_uid",
58-
"list_contact_points",
58+
"alerting_manage_routing",
5959
"list_incidents",
6060
"get_incident",
6161
"get_sift_investigation",
@@ -105,7 +105,7 @@ async def test_without_disable_write_flag_enables_write_tools(grafana_env):
105105
"get_dashboard_by_uid",
106106
"list_alert_rules",
107107
"get_alert_rule_by_uid",
108-
"list_contact_points",
108+
"alerting_manage_routing",
109109
"list_incidents",
110110
"get_incident",
111111
"get_sift_investigation",

tools/alerting.go

Lines changed: 1 addition & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"github.com/grafana/grafana-openapi-client-go/models"
1212
"github.com/mark3labs/mcp-go/mcp"
1313
"github.com/mark3labs/mcp-go/server"
14-
"github.com/prometheus/alertmanager/config"
1514
v1 "github.com/prometheus/client_golang/api/prometheus/v1"
1615
"github.com/prometheus/prometheus/model/labels"
1716

@@ -408,166 +407,6 @@ var GetAlertRuleByUID = mcpgrafana.MustTool(
408407
mcp.WithReadOnlyHintAnnotation(true),
409408
)
410409

411-
type ListContactPointsParams struct {
412-
DatasourceUID *string `json:"datasourceUid,omitempty" jsonschema:"description=Optional: UID of an Alertmanager-compatible datasource to query for receivers. If omitted\\, returns Grafana-managed contact points."`
413-
Limit int `json:"limit,omitempty" jsonschema:"description=The maximum number of results to return. Default is 100."`
414-
Name *string `json:"name,omitempty" jsonschema:"description=Filter contact points by name"`
415-
}
416-
417-
func (p ListContactPointsParams) validate() error {
418-
if p.Limit < 0 {
419-
return fmt.Errorf("invalid limit: %d, must be greater than 0", p.Limit)
420-
}
421-
return nil
422-
}
423-
424-
type contactPointSummary struct {
425-
UID string `json:"uid"`
426-
Name string `json:"name"`
427-
Type *string `json:"type,omitempty"`
428-
}
429-
430-
func listContactPoints(ctx context.Context, args ListContactPointsParams) ([]contactPointSummary, error) {
431-
if err := args.validate(); err != nil {
432-
return nil, fmt.Errorf("list contact points: %w", err)
433-
}
434-
435-
// If datasourceUID provided, query Alertmanager receivers
436-
if args.DatasourceUID != nil && *args.DatasourceUID != "" {
437-
return listAlertmanagerReceivers(ctx, args)
438-
}
439-
440-
c := mcpgrafana.GrafanaClientFromContext(ctx)
441-
442-
params := provisioning.NewGetContactpointsParams().WithContext(ctx)
443-
if args.Name != nil {
444-
params.Name = args.Name
445-
}
446-
447-
response, err := c.Provisioning.GetContactpoints(params)
448-
if err != nil {
449-
return nil, fmt.Errorf("list contact points: %w", err)
450-
}
451-
452-
filteredContactPoints, err := applyLimitToContactPoints(response.Payload, args.Limit)
453-
if err != nil {
454-
return nil, fmt.Errorf("list contact points: %w", err)
455-
}
456-
457-
return summarizeContactPoints(filteredContactPoints), nil
458-
}
459-
460-
func summarizeContactPoints(contactPoints []*models.EmbeddedContactPoint) []contactPointSummary {
461-
result := make([]contactPointSummary, 0, len(contactPoints))
462-
for _, cp := range contactPoints {
463-
result = append(result, contactPointSummary{
464-
UID: cp.UID,
465-
Name: cp.Name,
466-
Type: cp.Type,
467-
})
468-
}
469-
return result
470-
}
471-
472-
func applyLimitToContactPoints(items []*models.EmbeddedContactPoint, limit int) ([]*models.EmbeddedContactPoint, error) {
473-
if limit == 0 {
474-
limit = DefaultListContactPointsLimit
475-
}
476-
477-
if limit > len(items) {
478-
return items, nil
479-
}
480-
481-
return items[:limit], nil
482-
}
483-
484-
// listAlertmanagerReceivers queries an Alertmanager datasource for its receivers
485-
func listAlertmanagerReceivers(ctx context.Context, args ListContactPointsParams) ([]contactPointSummary, error) {
486-
dsUID := *args.DatasourceUID
487-
488-
// verify datasource exists and is Alertmanager type
489-
ds, err := getDatasourceByUID(ctx, GetDatasourceByUIDParams{UID: dsUID})
490-
if err != nil {
491-
return nil, fmt.Errorf("datasource %s: %w", dsUID, err)
492-
}
493-
494-
if !isAlertmanagerDatasource(ds.Type) {
495-
return nil, fmt.Errorf("datasource %s (type: %s) is not an Alertmanager datasource", dsUID, ds.Type)
496-
}
497-
498-
implementation := "prometheus" // default
499-
if ds.JSONData != nil {
500-
if jsonDataMap, ok := ds.JSONData.(map[string]interface{}); ok {
501-
if impl, ok := jsonDataMap["implementation"].(string); ok && impl != "" {
502-
implementation = impl
503-
}
504-
}
505-
}
506-
507-
client, err := newAlertingClientFromContext(ctx)
508-
if err != nil {
509-
return nil, fmt.Errorf("creating alerting client: %w", err)
510-
}
511-
512-
cfg, err := client.GetAlertmanagerConfig(ctx, dsUID, implementation)
513-
if err != nil {
514-
return nil, fmt.Errorf("querying Alertmanager config: %w", err)
515-
}
516-
517-
receivers := convertReceiversToContactPoints(cfg.Receivers)
518-
519-
if args.Name != nil && *args.Name != "" {
520-
receivers = filterContactPointsByName(receivers, *args.Name)
521-
}
522-
523-
if args.Limit > 0 && len(receivers) > args.Limit {
524-
receivers = receivers[:args.Limit]
525-
} else if args.Limit == 0 && len(receivers) > DefaultListContactPointsLimit {
526-
receivers = receivers[:DefaultListContactPointsLimit]
527-
}
528-
529-
return receivers, nil
530-
}
531-
532-
// isAlertmanagerDatasource checks if datasource type is Alertmanager
533-
func isAlertmanagerDatasource(dsType string) bool {
534-
dsType = strings.ToLower(dsType)
535-
return strings.Contains(dsType, "alertmanager")
536-
}
537-
538-
// convertReceiversToContactPoints converts Alertmanager receivers to contact point summaries
539-
// note: not really that useful, it's only giving the receiver name. We should refactor
540-
// contactPointSummary to include more data (url, email address)
541-
func convertReceiversToContactPoints(receivers []config.Receiver) []contactPointSummary {
542-
result := make([]contactPointSummary, 0, len(receivers))
543-
for _, r := range receivers {
544-
result = append(result, contactPointSummary{
545-
Name: r.Name,
546-
})
547-
}
548-
return result
549-
}
550-
551-
// filterContactPointsByName filters contact points by exact name match
552-
func filterContactPointsByName(cps []contactPointSummary, name string) []contactPointSummary {
553-
var filtered []contactPointSummary
554-
for _, cp := range cps {
555-
if cp.Name == name {
556-
filtered = append(filtered, cp)
557-
}
558-
}
559-
return filtered
560-
}
561-
562-
var ListContactPoints = mcpgrafana.MustTool(
563-
"list_contact_points",
564-
"Lists Grafana notification contact points, returning a summary including UID, name, and type for each. Optionally query Alertmanager receivers by providing datasourceUid. Supports filtering by name - exact match - and limiting the number of results.",
565-
listContactPoints,
566-
mcp.WithTitleAnnotation("List notification contact points"),
567-
mcp.WithIdempotentHintAnnotation(true),
568-
mcp.WithReadOnlyHintAnnotation(true),
569-
)
570-
571410
type CreateAlertRuleParams struct {
572411
Title string `json:"title" jsonschema:"required,description=The title of the alert rule"`
573412
RuleGroup string `json:"ruleGroup" jsonschema:"required,description=The rule group name"`
@@ -838,5 +677,5 @@ func AddAlertingTools(mcp *server.MCPServer, enableWriteTools bool) {
838677
UpdateAlertRule.Register(mcp)
839678
DeleteAlertRule.Register(mcp)
840679
}
841-
ListContactPoints.Register(mcp)
680+
ManageRouting.Register(mcp)
842681
}

tools/alerting_contact_points.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package tools
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/grafana/grafana-openapi-client-go/client/provisioning"
9+
"github.com/grafana/grafana-openapi-client-go/models"
10+
"github.com/prometheus/alertmanager/config"
11+
12+
mcpgrafana "github.com/grafana/mcp-grafana"
13+
)
14+
15+
type ListContactPointsParams struct {
16+
DatasourceUID *string `json:"datasourceUid,omitempty" jsonschema:"description=Optional: UID of an Alertmanager-compatible datasource to query for receivers. If omitted\\, returns Grafana-managed contact points."`
17+
Limit int `json:"limit,omitempty" jsonschema:"description=The maximum number of results to return. Default is 100."`
18+
Name *string `json:"name,omitempty" jsonschema:"description=Filter contact points by name"`
19+
}
20+
21+
func (p ListContactPointsParams) validate() error {
22+
if p.Limit < 0 {
23+
return fmt.Errorf("invalid limit: %d, must be greater than 0", p.Limit)
24+
}
25+
return nil
26+
}
27+
28+
type contactPointSummary struct {
29+
UID string `json:"uid"`
30+
Name string `json:"name"`
31+
Type *string `json:"type,omitempty"`
32+
}
33+
34+
func listContactPoints(ctx context.Context, args ListContactPointsParams) ([]contactPointSummary, error) {
35+
if err := args.validate(); err != nil {
36+
return nil, fmt.Errorf("list contact points: %w", err)
37+
}
38+
39+
if args.DatasourceUID != nil && *args.DatasourceUID != "" {
40+
return listAlertmanagerReceivers(ctx, args)
41+
}
42+
43+
c := mcpgrafana.GrafanaClientFromContext(ctx)
44+
45+
params := provisioning.NewGetContactpointsParams().WithContext(ctx)
46+
if args.Name != nil {
47+
params.Name = args.Name
48+
}
49+
50+
response, err := c.Provisioning.GetContactpoints(params)
51+
if err != nil {
52+
return nil, fmt.Errorf("list contact points: %w", err)
53+
}
54+
55+
filteredContactPoints, err := applyLimitToContactPoints(response.Payload, args.Limit)
56+
if err != nil {
57+
return nil, fmt.Errorf("list contact points: %w", err)
58+
}
59+
60+
return summarizeContactPoints(filteredContactPoints), nil
61+
}
62+
63+
func summarizeContactPoints(contactPoints []*models.EmbeddedContactPoint) []contactPointSummary {
64+
result := make([]contactPointSummary, 0, len(contactPoints))
65+
for _, cp := range contactPoints {
66+
result = append(result, contactPointSummary{
67+
UID: cp.UID,
68+
Name: cp.Name,
69+
Type: cp.Type,
70+
})
71+
}
72+
return result
73+
}
74+
75+
func applyLimitToContactPoints(items []*models.EmbeddedContactPoint, limit int) ([]*models.EmbeddedContactPoint, error) {
76+
if limit == 0 {
77+
limit = DefaultListContactPointsLimit
78+
}
79+
80+
if limit > len(items) {
81+
return items, nil
82+
}
83+
84+
return items[:limit], nil
85+
}
86+
87+
func listAlertmanagerReceivers(ctx context.Context, args ListContactPointsParams) ([]contactPointSummary, error) {
88+
dsUID := *args.DatasourceUID
89+
90+
ds, err := getDatasourceByUID(ctx, GetDatasourceByUIDParams{UID: dsUID})
91+
if err != nil {
92+
return nil, fmt.Errorf("datasource %s: %w", dsUID, err)
93+
}
94+
95+
if !isAlertmanagerDatasource(ds.Type) {
96+
return nil, fmt.Errorf("datasource %s (type: %s) is not an Alertmanager datasource", dsUID, ds.Type)
97+
}
98+
99+
implementation := "prometheus"
100+
if ds.JSONData != nil {
101+
if jsonDataMap, ok := ds.JSONData.(map[string]interface{}); ok {
102+
if impl, ok := jsonDataMap["implementation"].(string); ok && impl != "" {
103+
implementation = impl
104+
}
105+
}
106+
}
107+
108+
client, err := newAlertingClientFromContext(ctx)
109+
if err != nil {
110+
return nil, fmt.Errorf("creating alerting client: %w", err)
111+
}
112+
113+
cfg, err := client.GetAlertmanagerConfig(ctx, dsUID, implementation)
114+
if err != nil {
115+
return nil, fmt.Errorf("querying Alertmanager config: %w", err)
116+
}
117+
118+
receivers := convertReceiversToContactPoints(cfg.Receivers)
119+
120+
if args.Name != nil && *args.Name != "" {
121+
receivers = filterContactPointsByName(receivers, *args.Name)
122+
}
123+
124+
if args.Limit > 0 && len(receivers) > args.Limit {
125+
receivers = receivers[:args.Limit]
126+
} else if args.Limit == 0 && len(receivers) > DefaultListContactPointsLimit {
127+
receivers = receivers[:DefaultListContactPointsLimit]
128+
}
129+
130+
return receivers, nil
131+
}
132+
133+
func isAlertmanagerDatasource(dsType string) bool {
134+
dsType = strings.ToLower(dsType)
135+
return strings.Contains(dsType, "alertmanager")
136+
}
137+
138+
func convertReceiversToContactPoints(receivers []config.Receiver) []contactPointSummary {
139+
result := make([]contactPointSummary, 0, len(receivers))
140+
for _, r := range receivers {
141+
result = append(result, contactPointSummary{
142+
Name: r.Name,
143+
})
144+
}
145+
return result
146+
}
147+
148+
func filterContactPointsByName(cps []contactPointSummary, name string) []contactPointSummary {
149+
var filtered []contactPointSummary
150+
for _, cp := range cps {
151+
if cp.Name == name {
152+
filtered = append(filtered, cp)
153+
}
154+
}
155+
return filtered
156+
}

0 commit comments

Comments
 (0)