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
6 changes: 3 additions & 3 deletions pkg/flashduty/alerts.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func QueryAlerts(getClient GetFlashdutyClientFn, t translations.TranslationHelpe
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("Maximum number of results to return."), mcp.DefaultNumber(20), mcp.Min(1), mcp.Max(100)),
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 {
Expand Down Expand Up @@ -118,12 +118,12 @@ func QueryAlerts(getClient GetFlashdutyClientFn, t translations.TranslationHelpe
return mcp.NewToolResultError(fmt.Sprintf("Unable to retrieve alerts: %v", err)), nil
}

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

Expand Down
6 changes: 3 additions & 3 deletions pkg/flashduty/changes.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func QueryChanges(getClient GetFlashdutyClientFn, t translations.TranslationHelp
WithSince(),
WithUntil(),
mcp.WithString("type", mcp.Description("Filter by change type.")),
mcp.WithNumber("limit", mcp.Description("Maximum number of results to return."), mcp.DefaultNumber(20), mcp.Min(1), mcp.Max(100)),
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 {
Expand Down Expand Up @@ -92,9 +92,9 @@ func QueryChanges(getClient GetFlashdutyClientFn, t translations.TranslationHelp
return mcp.NewToolResultError(fmt.Sprintf("Unable to retrieve changes: %v", err)), nil
}

return MarshalResult(map[string]any{
return MarshalResult(addTruncationHint(map[string]any{
"changes": output.Changes,
"total": output.Total,
}), nil
}, len(output.Changes), output.Total)), nil
}
}
8 changes: 4 additions & 4 deletions pkg/flashduty/channels.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,10 @@ func QueryChannels(getClient GetFlashdutyClientFn, t translations.TranslationHel
return mcp.NewToolResultError(fmt.Sprintf("Unable to retrieve channels: %v", err)), nil
}

return MarshalResult(map[string]any{
return MarshalResult(addTruncationHint(map[string]any{
"channels": output.Channels,
"total": output.Total,
}), nil
}, len(output.Channels), output.Total)), nil
}
}

Expand Down Expand Up @@ -89,9 +89,9 @@ func QueryEscalationRules(getClient GetFlashdutyClientFn, t translations.Transla
return mcp.NewToolResultError(fmt.Sprintf("Unable to query escalation rules: %v", err)), nil
}

return MarshalResult(map[string]any{
return MarshalResult(addTruncationHint(map[string]any{
"rules": output.Rules,
"total": output.Total,
}), nil
}, len(output.Rules), output.Total)), nil
}
}
4 changes: 2 additions & 2 deletions pkg/flashduty/fields.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ func QueryFields(getClient GetFlashdutyClientFn, t translations.TranslationHelpe
return mcp.NewToolResultError(fmt.Sprintf("Unable to retrieve fields: %v", err)), nil
}

return MarshalResult(map[string]any{
return MarshalResult(addTruncationHint(map[string]any{
"fields": output.Fields,
"total": output.Total,
}), nil
}, len(output.Fields), output.Total)), nil
}
}
10 changes: 5 additions & 5 deletions pkg/flashduty/incidents.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func QueryIncidents(getClient GetFlashdutyClientFn, t translations.TranslationHe
WithSince(),
WithUntil(),
mcp.WithString("title", mcp.Description("Keyword search in incident title.")),
mcp.WithNumber("limit", mcp.Description("Maximum number of results to return."), mcp.DefaultNumber(20), mcp.Min(1), mcp.Max(100)),
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) {
ctx, client, err := getClient(ctx)
Expand Down Expand Up @@ -104,10 +104,10 @@ func QueryIncidents(getClient GetFlashdutyClientFn, t translations.TranslationHe
return mcp.NewToolResultError(fmt.Sprintf("Unable to retrieve incidents: %v", err)), nil
}

return MarshalResult(map[string]any{
return MarshalResult(addTruncationHint(map[string]any{
"incidents": output.Incidents,
"total": output.Total,
}), nil
}, len(output.Incidents), output.Total)), nil
}
}

Expand Down Expand Up @@ -456,9 +456,9 @@ func ListSimilarIncidents(getClient GetFlashdutyClientFn, t translations.Transla
return mcp.NewToolResultError(fmt.Sprintf("Unable to find similar incidents: %v", err)), nil
}

return MarshalResult(map[string]any{
return MarshalResult(addTruncationHint(map[string]any{
"incidents": output.Incidents,
"total": output.Total,
}), nil
}, len(output.Incidents), output.Total)), nil
}
}
30 changes: 30 additions & 0 deletions pkg/flashduty/pagination.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package flashduty

import "fmt"

// LimitDescription is the canonical description for the `limit` parameter on
// list-shaped tools. Mentioning `truncated`/`total` up front primes the LLM to
// inspect them before assuming it has the full result.
const LimitDescription = "Maximum number of results to return. Default 20, max 100. " +
"When more results exist than were returned, the response carries " +
"`truncated:true` and a `hint` field with concrete next steps."

// addTruncationHint stamps `truncated: true` and a human-readable `hint` onto
// list-shaped results when the returned slice is shorter than the backend's
// total. Without these explicit fields the LLM has to remember to compare
// `len(items) < total`, and skipping that check is the most common cause of
// "the LLM only looked at the first 20 incidents and missed the obvious one"
// reports.
//
// No-op when nothing was truncated, so happy-path output stays clean.
// Returns the same map for one-line use.
func addTruncationHint(res map[string]any, count, total int) map[string]any {
if total > count {
res["truncated"] = true
res["hint"] = fmt.Sprintf(
"Returned %d of %d total. To see more: raise `limit` (max 100), narrow `since`/`until`, or add filters (severity, channel_ids, etc.).",
count, total,
)
}
return res
}
4 changes: 2 additions & 2 deletions pkg/flashduty/statuspage.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,10 @@ func ListStatusChanges(getClient GetFlashdutyClientFn, t translations.Translatio
return mcp.NewToolResultError(fmt.Sprintf("failed to list status changes: %v", err)), nil
}

return MarshalResult(map[string]any{
return MarshalResult(addTruncationHint(map[string]any{
"changes": output.Changes,
"total": output.Total,
}), nil
}, len(output.Changes), output.Total)), nil
}
}

Expand Down
12 changes: 7 additions & 5 deletions pkg/flashduty/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,17 @@ func QueryMembers(getClient GetFlashdutyClientFn, t translations.TranslationHelp
return mcp.NewToolResultError(fmt.Sprintf("Unable to retrieve members: %v", err)), nil
}

members := any(output.Members)
var members any = output.Members
count := len(output.Members)
if len(output.PersonInfos) > 0 {
members = output.PersonInfos
count = len(output.PersonInfos)
}

return MarshalResult(map[string]any{
return MarshalResult(addTruncationHint(map[string]any{
"members": members,
"total": output.Total,
}), nil
}, count, output.Total)), nil
}
}

Expand Down Expand Up @@ -117,9 +119,9 @@ func QueryTeams(getClient GetFlashdutyClientFn, t translations.TranslationHelper
}), nil
}

return MarshalResult(map[string]any{
return MarshalResult(addTruncationHint(map[string]any{
"teams": output.Teams,
"total": output.Total,
}), nil
}, len(output.Teams), output.Total)), nil
}
}
Loading