diff --git a/go.mod b/go.mod index 9bd41ce..96fe70a 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 9dfcbfc..7ab8618 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/flashduty/alerts.go b/pkg/flashduty/alerts.go index 6f5a924..b0b5408 100644 --- a/pkg/flashduty/alerts.go +++ b/pkg/flashduty/alerts.go @@ -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. diff --git a/pkg/flashduty/incidents.go b/pkg/flashduty/incidents.go index 8b447de..cd4f0c4 100644 --- a/pkg/flashduty/incidents.go +++ b/pkg/flashduty/incidents.go @@ -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) { @@ -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) { @@ -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"]) @@ -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, } diff --git a/pkg/flashduty/time_args.go b/pkg/flashduty/time_args.go index 5d2d917..3ebca32 100644 --- a/pkg/flashduty/time_args.go +++ b/pkg/flashduty/time_args.go @@ -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. diff --git a/pkg/flashduty/tools.go b/pkg/flashduty/tools.go index 836a7c8..df55e35 100644 --- a/pkg/flashduty/tools.go +++ b/pkg/flashduty/tools.go @@ -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)