Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.25.5

require (
github.com/bluele/gcache v0.0.2
github.com/flashcatcloud/flashduty-sdk v0.8.0
github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260510030603-7a1f724ceb79
github.com/google/go-github/v72 v72.0.0
github.com/josephburnett/jd v1.9.2
github.com/mark3labs/mcp-go v0.52.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/flashcatcloud/flashduty-sdk v0.8.0 h1:BMLCSwZjVK/WURSSNJdSlfe1F5bwmPunwkwTQsTY9+w=
github.com/flashcatcloud/flashduty-sdk v0.8.0/go.mod h1:dG4eJfdZaj4jNBMwEexbfK/3PmcIMhNeJ88L/DcZzUY=
github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260510030603-7a1f724ceb79 h1:VpHQKfWBw2hMKScvGvF/u7jUug44qk2ALD/E0v88ohM=
github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260510030603-7a1f724ceb79/go.mod h1:dG4eJfdZaj4jNBMwEexbfK/3PmcIMhNeJ88L/DcZzUY=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
Expand Down
116 changes: 0 additions & 116 deletions pkg/flashduty/alerts.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,131 +2,15 @@ package flashduty

import (
"context"
"encoding/json"
"fmt"

sdk "github.com/flashcatcloud/flashduty-sdk"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"

"github.com/flashcatcloud/flashduty-mcp-server/internal/timeutil"
"github.com/flashcatcloud/flashduty-mcp-server/pkg/translations"
)

const queryAlertsDescription = `Query alerts by time range and filters. Returns enriched data with channel/integration names. Useful for finding active or historical alerts that fed into incidents.`

// QueryAlerts creates a tool to query alerts with enriched data.
func QueryAlerts(getClient GetFlashdutyClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("query_alerts",
mcp.WithDescription(t("TOOL_QUERY_ALERTS_DESCRIPTION", queryAlertsDescription)),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_QUERY_ALERTS_USER_TITLE", "Query alerts"),
ReadOnlyHint: ToBoolPtr(true),
}),
WithSince(mcp.Required()),
WithUntil(mcp.Required()),
mcp.WithString("severity", mcp.Description("Filter by alert severity."), mcp.Enum("Info", "Warning", "Critical")),
mcp.WithBoolean("is_active", mcp.Description("If true, only return alerts that are currently active (Triggered or Processing). If false, only inactive (Closed). If omitted, returns all.")),
mcp.WithString("channel_ids", mcp.Description("Comma-separated collaboration space IDs to filter by.")),
mcp.WithString("integration_ids", mcp.Description("Comma-separated integration IDs to filter by.")),
mcp.WithString("alert_keys", mcp.Description("Comma-separated alert dedup keys for direct lookup.")),
mcp.WithBoolean("ever_muted", mcp.Description("If true, only return alerts that were ever muted by a routing rule.")),
mcp.WithString("title", mcp.Description("Keyword search in alert title.")),
mcp.WithString("labels", mcp.Description("JSON object of label key-value pairs to match. Format: {\"resource\":\"web-01\",\"region\":\"us-west\"}.")),
mcp.WithNumber("limit", mcp.Description(LimitDescription), mcp.DefaultNumber(20), mcp.Min(1), mcp.Max(100)),
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
ctx, client, err := getClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get Flashduty client: %w", err)
}

args := request.GetArguments()

startTime, err := timeutil.ParseAny(args["since"])
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("invalid since: %v", err)), nil
}
endTime, err := timeutil.ParseAny(args["until"])
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("invalid until: %v", err)), nil
}
if err := validateTimeWindow(startTime, endTime); err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

severity, _ := OptionalParam[string](request, "severity")
channelIdsStr, _ := OptionalParam[string](request, "channel_ids")
integrationIdsStr, _ := OptionalParam[string](request, "integration_ids")
alertKeysStr, _ := OptionalParam[string](request, "alert_keys")
title, _ := OptionalParam[string](request, "title")
labelsStr, _ := OptionalParam[string](request, "labels")
limit, _ := OptionalInt(request, "limit")
if limit <= 0 {
limit = defaultQueryLimit
}

input := &sdk.ListAlertsInput{
StartTime: startTime,
EndTime: endTime,
AlertSeverity: severity,
Title: title,
Limit: limit,
}

if v, ok := args["is_active"].(bool); ok {
input.IsActive = &v
}
if v, ok := args["ever_muted"].(bool); ok {
input.EverMuted = &v
}

if channelIdsStr != "" {
ids := parseCommaSeparatedInts(channelIdsStr)
if len(ids) == 0 {
return mcp.NewToolResultError("channel_ids must contain at least one valid ID when specified"), nil
}
input.ChannelIDs = make([]int64, len(ids))
for i, id := range ids {
input.ChannelIDs[i] = int64(id)
}
}
if integrationIdsStr != "" {
ids := parseCommaSeparatedInts(integrationIdsStr)
if len(ids) == 0 {
return mcp.NewToolResultError("integration_ids must contain at least one valid ID when specified"), nil
}
input.IntegrationIDs = make([]int64, len(ids))
for i, id := range ids {
input.IntegrationIDs[i] = int64(id)
}
}
if alertKeysStr != "" {
input.AlertKeys = parseCommaSeparatedStrings(alertKeysStr)
}
if labelsStr != "" {
labels := map[string]string{}
if err := json.Unmarshal([]byte(labelsStr), &labels); err != nil {
return mcp.NewToolResultError(fmt.Sprintf("invalid labels JSON: %v", err)), nil
}
if len(labels) > 0 {
input.Labels = labels
}
}

output, err := client.ListAlerts(ctx, input)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Unable to retrieve alerts: %v", err)), nil
}

return MarshalResult(addTruncationHint(map[string]any{
"alerts": output.Alerts,
"total": output.Total,
"has_next_page": output.HasNextPage,
"search_after_ctx": output.SearchAfterCtx,
}, len(output.Alerts), output.Total)), nil
}
}

const queryAlertEventsDescription = `Query raw events for a single alert. Returns the upstream event stream that produced the alert (e.g. each individual Prometheus firing).`

// QueryAlertEvents creates a tool to query raw events of a single alert.
Expand Down
11 changes: 5 additions & 6 deletions pkg/flashduty/incidents.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (

const defaultQueryLimit = 20

const queryIncidentsDescription = `Query incidents by IDs, time range, status, severity, or channel. Returns enriched data with names.`
const queryIncidentsDescription = `Query incidents by IDs, time range, status, severity, channel, or free-text query. Returns enriched data with names.`

// QueryIncidents creates a tool to query incidents with enriched data
func QueryIncidents(getClient GetFlashdutyClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
Expand All @@ -32,7 +32,7 @@ func QueryIncidents(getClient GetFlashdutyClientFn, t translations.TranslationHe
mcp.WithString("channel_ids", mcp.Description("Comma-separated collaboration space IDs to filter by. Backend expects an array — singular channel_id is silently ignored.")),
WithSince(),
WithUntil(),
mcp.WithString("title", mcp.Description("Keyword search in incident title.")),
mcp.WithString("query", mcp.Description("Free-text search across title, labels, and content (Doris full-text). A 24-char hex string is resolved as an incident ID; a 6-char string is resolved as an incident num. Prefer this over picking exact filter values when the user gives a fuzzy keyword."), mcp.MaxLength(200)),
mcp.WithNumber("limit", mcp.Description(LimitDescription), mcp.DefaultNumber(20), mcp.Min(1), mcp.Max(100)),
mcp.WithBoolean("include_alerts", mcp.Description("Whether to include alerts preview (first 20 alerts with total count)."), mcp.DefaultBool(true)),
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
Expand All @@ -41,13 +41,12 @@ func QueryIncidents(getClient GetFlashdutyClientFn, t translations.TranslationHe
return nil, fmt.Errorf("failed to get Flashduty client: %w", err)
}

// Extract parameters
args := request.GetArguments()
incidentIdsStr, _ := OptionalParam[string](request, "incident_ids")
progress, _ := OptionalParam[string](request, "progress")
severity, _ := OptionalParam[string](request, "severity")
channelIdsStr, _ := OptionalParam[string](request, "channel_ids")
args := request.GetArguments()
title, _ := OptionalParam[string](request, "title")
query, _ := OptionalParam[string](request, "query")
limit, _ := OptionalInt(request, "limit")

startTime, err := timeutil.ParseAny(args["since"])
Expand All @@ -73,7 +72,7 @@ func QueryIncidents(getClient GetFlashdutyClientFn, t translations.TranslationHe
Severity: severity,
StartTime: startTime,
EndTime: endTime,
Title: title,
Query: query,
Limit: limit,
IncludeAlerts: includeAlerts,
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/flashduty/time_args.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import (
// so the LLM gets a guided error before the round-trip.
const MaxTimeWindow = 31 * 24 * time.Hour

// SinceDescription / UntilDescription are reused across query_incidents,
// query_alerts, and query_changes. The wording is tuned for LLM callers that
// SinceDescription / UntilDescription are reused across query_incidents
// and query_changes. The wording is tuned for LLM callers that
// otherwise pick absolute dates from stale training data and silently query
// the wrong year — see the three failure modes documented at
// https://github.com/flashcatcloud/flashduty-mcp-server/pull/50.
Expand Down
3 changes: 1 addition & 2 deletions pkg/flashduty/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,9 @@ func DefaultToolsetGroup(getClient GetFlashdutyClientFn, readOnly bool, t transl
)
group.AddToolset(incidents)

// Alerts toolset (2 tools)
// Alerts toolset (1 tool)
alerts := toolsets.NewToolset("alerts", "Alert query tools").
AddReadTools(
toolsets.NewServerTool(QueryAlerts(getClient, t)),
toolsets.NewServerTool(QueryAlertEvents(getClient, t)),
)
group.AddToolset(alerts)
Expand Down
Loading