From 26e39b41b3903c5694f1bba5ce122b7079111fed Mon Sep 17 00:00:00 2001 From: ysyneu Date: Sun, 10 May 2026 11:07:40 +0800 Subject: [PATCH 1/2] feat: drop query_alerts and switch query_incidents to free-text query The query_alerts tool nudged LLMs into iterating over individual alert events instead of working with incidents (the actual primary entity in Flashduty). Remove it; QueryAlertEvents is preserved for drilling into a single alert by ID, which still has clear value during incident investigation. For query_incidents, replace the title param with query, which maps to /incident/list's `query` field. Backend behavior: - Doris full-text search across title/labels/content - 24-char hex string -> resolved as incident_id - 6-char string -> resolved as incident num The previous title param was exact-match by default and surfaced almost nothing for fuzzy keywords coming out of an LLM. query is the same field the web UI search bar uses. go.mod replace pins to the SDK fork commit while flashcatcloud/flashduty-sdk#5 is in review; swap to a real version once that PR merges. --- go.mod | 2 + go.sum | 4 +- pkg/flashduty/alerts.go | 116 ------------------------------------- pkg/flashduty/incidents.go | 11 ++-- pkg/flashduty/time_args.go | 4 +- pkg/flashduty/tools.go | 3 +- 6 files changed, 12 insertions(+), 128 deletions(-) diff --git a/go.mod b/go.mod index 9bd41ce..3cb7116 100644 --- a/go.mod +++ b/go.mod @@ -50,3 +50,5 @@ require ( golang.org/x/text v0.26.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/flashcatcloud/flashduty-sdk => github.com/ysyneu/flashduty-sdk v0.7.1-0.20260510030603-7a1f724ceb79 diff --git a/go.sum b/go.sum index 9dfcbfc..7b29735 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,6 @@ 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/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= @@ -89,6 +87,8 @@ github.com/toon-format/toon-go v0.0.0-20251202084852-7ca0e27c4e8c h1:D8lDFovBMZy github.com/toon-format/toon-go v0.0.0-20251202084852-7ca0e27c4e8c/go.mod h1:j/BOnpF2ihnz4lELs99h9mwGJBx/zdleOUCnLLRPCsc= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +github.com/ysyneu/flashduty-sdk v0.7.1-0.20260510030603-7a1f724ceb79 h1:RalVjW6XT4qNmd5wglTjwXhrQl0eUGLB9TCE7Q/ak3o= +github.com/ysyneu/flashduty-sdk v0.7.1-0.20260510030603-7a1f724ceb79/go.mod h1:dG4eJfdZaj4jNBMwEexbfK/3PmcIMhNeJ88L/DcZzUY= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 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) From e53fa99c0ebe1fa1ec3247900df2cd1519a90a91 Mon Sep 17 00:00:00 2001 From: ysyneu Date: Sun, 10 May 2026 11:21:04 +0800 Subject: [PATCH 2/2] build: pin flashduty-sdk to upstream commit, drop fork replace The previous commit pinned via a replace pointing at the ysyneu fork because the SDK PR was open cross-fork. The SDK branch is now also on flashcatcloud/flashduty-sdk, so we can pin straight to the upstream commit pseudo-version and drop the replace. --- go.mod | 4 +--- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 3cb7116..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 @@ -50,5 +50,3 @@ require ( golang.org/x/text v0.26.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) - -replace github.com/flashcatcloud/flashduty-sdk => github.com/ysyneu/flashduty-sdk v0.7.1-0.20260510030603-7a1f724ceb79 diff --git a/go.sum b/go.sum index 7b29735..7ab8618 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +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.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= @@ -87,8 +89,6 @@ github.com/toon-format/toon-go v0.0.0-20251202084852-7ca0e27c4e8c h1:D8lDFovBMZy github.com/toon-format/toon-go v0.0.0-20251202084852-7ca0e27c4e8c/go.mod h1:j/BOnpF2ihnz4lELs99h9mwGJBx/zdleOUCnLLRPCsc= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= -github.com/ysyneu/flashduty-sdk v0.7.1-0.20260510030603-7a1f724ceb79 h1:RalVjW6XT4qNmd5wglTjwXhrQl0eUGLB9TCE7Q/ak3o= -github.com/ysyneu/flashduty-sdk v0.7.1-0.20260510030603-7a1f724ceb79/go.mod h1:dG4eJfdZaj4jNBMwEexbfK/3PmcIMhNeJ88L/DcZzUY= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=