diff --git a/pkg/flashduty/incidents.go b/pkg/flashduty/incidents.go index 2221e08..8f06748 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, channel, or free-text query. Returns the incident list with an alerts_total count per incident; for the actual alert objects of one or more incidents, call query_incident_alerts(incident_ids=...).` +const queryIncidentsDescription = `Query incidents by IDs, short ids (nums), time range, status, severity, channel, or free-text query. Returns the incident list with an alerts_total count per incident; for the actual alert objects of one or more incidents, call query_incident_alerts(incident_ids=...).` // QueryIncidents creates a tool to query incidents with enriched data func QueryIncidents(getClient GetFlashdutyClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { @@ -33,6 +33,7 @@ func QueryIncidents(getClient GetFlashdutyClientFn, t translations.TranslationHe WithSince(), WithUntil(), 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.WithString("nums", mcp.Description("Comma-separated short incident ids (num — the 6-char id shown in the UI, e.g. 311510). Matched within the since/until window; the backend caps the list span at ~30 days, so incidents older than that must be looked up by their full incident_id.")), 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) @@ -46,6 +47,7 @@ func QueryIncidents(getClient GetFlashdutyClientFn, t translations.TranslationHe severity, _ := OptionalParam[string](request, "severity") channelIdsStr, _ := OptionalParam[string](request, "channel_ids") query, _ := OptionalParam[string](request, "query") + nums, _ := OptionalParam[string](request, "nums") limit, _ := OptionalInt(request, "limit") startTime, err := timeutil.ParseAny(args["since"]) @@ -109,6 +111,10 @@ func QueryIncidents(getClient GetFlashdutyClientFn, t translations.TranslationHe } } + if nums != "" { + req.Nums = parseCommaSeparatedStrings(nums) + } + if err := validateTimeWindow(startTime, endTime); err != nil { return mcp.NewToolResultError(err.Error()), nil } diff --git a/pkg/flashduty/incidents_nums_test.go b/pkg/flashduty/incidents_nums_test.go new file mode 100644 index 0000000..c4cea3d --- /dev/null +++ b/pkg/flashduty/incidents_nums_test.go @@ -0,0 +1,68 @@ +package flashduty + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + flashduty "github.com/flashcatcloud/go-flashduty" + "github.com/mark3labs/mcp-go/mcp" + + "github.com/flashcatcloud/flashduty-mcp-server/pkg/translations" +) + +// TestQueryIncidentsNumsReachesWire verifies the new `nums` argument is split +// and sent to /incident/list as the nums array (short-id filtering), within the +// since/until window. +func TestQueryIncidentsNumsReachesWire(t *testing.T) { + t.Parallel() + + var gotPath string + var gotBody map[string]any + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotPath = r.URL.Path + _ = json.NewDecoder(r.Body).Decode(&gotBody) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{"items": []any{}, "total": 0}, + }) + })) + defer ts.Close() + + client, err := flashduty.NewClient("test-key", flashduty.WithBaseURL(ts.URL)) + if err != nil { + t.Fatalf("new go-flashduty client: %v", err) + } + + _, handler := QueryIncidents(func(ctx context.Context) (context.Context, *Clients, error) { + return ctx, &Clients{New: client}, nil + }, translations.NullTranslationHelper) + + result, err := handler(context.Background(), mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: "query_incidents", + Arguments: map[string]any{ + "nums": "311510,ABC123", + "since": "7d", + "until": "now", + }, + }, + }) + if err != nil { + t.Fatalf("handler returned error: %v", err) + } + if result.IsError { + txt, _ := mcp.AsTextContent(result.Content[0]) + t.Fatalf("expected success result, got error: %s", txt.Text) + } + + if gotPath != "/incident/list" { + t.Fatalf("path = %q, want /incident/list", gotPath) + } + nums, _ := gotBody["nums"].([]any) + if len(nums) != 2 || nums[0] != "311510" || nums[1] != "ABC123" { + t.Errorf("nums = %#v, want [311510 ABC123]", gotBody["nums"]) + } +}