Skip to content

Commit e0bc20f

Browse files
A-GeorgiouCopilot
andcommitted
Page list_label with cursors instead of fetching every label
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d8a9000c-ebe8-4118-ae89-a942861e4166
1 parent 7311be6 commit e0bc20f

6 files changed

Lines changed: 176 additions & 158 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,7 +1126,9 @@ For negotiated supported protocol versions `2026-07-28` or newer, the consolidat
11261126

11271127
- **list_label** - List labels from a repository
11281128
- **OAuth Challenge Scopes**: `repo`
1129+
- `after`: Cursor for pagination. Use the cursor from the previous response. (string, optional)
11291130
- `owner`: Repository owner (username or organization name) - required for all operations (string, required)
1131+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
11301132
- `repo`: Repository name - required for all operations (string, required)
11311133

11321134
</details>

‎pkg/github/__toolsnaps__/list_label.snap‎

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,23 @@
44
"readOnlyHint": true,
55
"title": "List labels from a repository"
66
},
7-
"description": "List all labels from a repository, ordered by issue count (descending) so the most-used labels are returned first. has_more is true when the repository has more labels than could be returned; use get_label to check a specific label by name.",
7+
"description": "List labels from a repository, ordered by issue count (descending) so the most-used labels are returned first. Returns up to 100 labels per page by default. If 'pageInfo.hasNextPage' is true, use the 'endCursor' from the previous response's 'pageInfo' in the 'after' parameter to get the rest. Use get_label to check a specific label by name.",
88
"inputSchema": {
99
"properties": {
10+
"after": {
11+
"description": "Cursor for pagination. Use the cursor from the previous response.",
12+
"type": "string"
13+
},
1014
"owner": {
1115
"description": "Repository owner (username or organization name) - required for all operations",
1216
"type": "string"
1317
},
18+
"perPage": {
19+
"description": "Results per page for pagination (min 1, max 100)",
20+
"maximum": 100,
21+
"minimum": 1,
22+
"type": "number"
23+
},
1424
"repo": {
1525
"description": "Repository name - required for all operations",
1626
"type": "string"
@@ -26,10 +36,6 @@
2636
"outputSchema": {
2737
"additionalProperties": false,
2838
"properties": {
29-
"has_more": {
30-
"description": "Whether the repository has more labels than were returned.",
31-
"type": "boolean"
32-
},
3339
"labels": {
3440
"items": {
3541
"additionalProperties": false,
@@ -57,6 +63,28 @@
5763
"array"
5864
]
5965
},
66+
"pageInfo": {
67+
"additionalProperties": false,
68+
"properties": {
69+
"endCursor": {
70+
"type": "string"
71+
},
72+
"hasNextPage": {
73+
"type": "boolean"
74+
},
75+
"hasPreviousPage": {
76+
"type": "boolean"
77+
},
78+
"startCursor": {
79+
"type": "string"
80+
}
81+
},
82+
"required": [
83+
"hasNextPage",
84+
"hasPreviousPage"
85+
],
86+
"type": "object"
87+
},
6088
"totalCount": {
6189
"description": "Total labels in the repository.",
6290
"type": "integer"
@@ -65,7 +93,7 @@
6593
"required": [
6694
"labels",
6795
"totalCount",
68-
"has_more"
96+
"pageInfo"
6997
],
7098
"type": "object"
7199
}

‎pkg/github/labels.go‎

Lines changed: 49 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,8 @@ func GetLabelForLabelsToolset(t translations.TranslationHelperFunc) inventory.Se
132132
return tool
133133
}
134134

135-
// listLabelsMaxPages bounds list_label to 1,000 labels so one call cannot page indefinitely.
136-
const listLabelsMaxPages = 10
135+
// listLabelsDefaultPerPage keeps the first page as large as GraphQL allows, so most repositories need one call.
136+
const listLabelsDefaultPerPage = 100
137137

138138
// ListLabels lists labels from a repository
139139
func ListLabels(t translations.TranslationHelperFunc) inventory.ServerTool {
@@ -145,12 +145,12 @@ func listLabelsLegacy(t translations.TranslationHelperFunc) inventory.ServerTool
145145
ToolsetLabels,
146146
mcp.Tool{
147147
Name: "list_label",
148-
Description: t("TOOL_LIST_LABEL_DESCRIPTION", "List all labels from a repository, ordered by issue count (descending) so the most-used labels are returned first. has_more is true when the repository has more labels than could be returned; use get_label to check a specific label by name."),
148+
Description: t("TOOL_LIST_LABEL_DESCRIPTION", "List labels from a repository, ordered by issue count (descending) so the most-used labels are returned first. Returns up to 100 labels per page by default. If 'pageInfo.hasNextPage' is true, use the 'endCursor' from the previous response's 'pageInfo' in the 'after' parameter to get the rest. Use get_label to check a specific label by name."),
149149
Annotations: &mcp.ToolAnnotations{
150150
Title: t("TOOL_LIST_LABEL_TITLE", "List labels from a repository"),
151151
ReadOnlyHint: true,
152152
},
153-
InputSchema: &jsonschema.Schema{
153+
InputSchema: WithCursorPagination(&jsonschema.Schema{
154154
Type: "object",
155155
Properties: map[string]*jsonschema.Schema{
156156
"owner": {
@@ -163,7 +163,7 @@ func listLabelsLegacy(t translations.TranslationHelperFunc) inventory.ServerTool
163163
},
164164
},
165165
Required: []string{"owner", "repo"},
166-
},
166+
}),
167167
},
168168
scopes.PublicRead(scopes.Repo),
169169
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
@@ -177,6 +177,19 @@ func listLabelsLegacy(t translations.TranslationHelperFunc) inventory.ServerTool
177177
return utils.NewToolResultError(err.Error()), nil, nil
178178
}
179179

180+
perPage, err := OptionalIntParamWithDefault(args, "perPage", listLabelsDefaultPerPage)
181+
if err != nil {
182+
return utils.NewToolResultError(err.Error()), nil, nil
183+
}
184+
after, err := OptionalParam[string](args, "after")
185+
if err != nil {
186+
return utils.NewToolResultError(err.Error()), nil, nil
187+
}
188+
paginationParams, err := CursorPaginationParams{PerPage: perPage, After: after}.ToGraphQLParams()
189+
if err != nil {
190+
return utils.NewToolResultError(err.Error()), nil, nil
191+
}
192+
180193
client, err := deps.GetGQLClient(ctx)
181194
if err != nil {
182195
return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
@@ -193,50 +206,53 @@ func listLabelsLegacy(t translations.TranslationHelperFunc) inventory.ServerTool
193206
}
194207
TotalCount githubv4.Int
195208
PageInfo struct {
196-
HasNextPage githubv4.Boolean
197-
EndCursor githubv4.String
209+
HasNextPage githubv4.Boolean
210+
HasPreviousPage githubv4.Boolean
211+
StartCursor githubv4.String
212+
EndCursor githubv4.String
198213
}
199-
} `graphql:"labels(first: 100, after: $cursor, orderBy: {field: $orderByField, direction: $orderByDirection})"`
214+
} `graphql:"labels(first: $first, after: $after, orderBy: {field: $orderByField, direction: $orderByDirection})"`
200215
} `graphql:"repository(owner: $owner, name: $repo)"`
201216
}
202217

203218
vars := map[string]any{
204-
"owner": githubv4.String(owner),
205-
"repo": githubv4.String(repo),
206-
"cursor": (*githubv4.String)(nil),
219+
"owner": githubv4.String(owner),
220+
"repo": githubv4.String(repo),
221+
"first": githubv4.Int(*paginationParams.First),
207222
// Order labels by issue count (descending) so the most-used labels are returned first.
208223
"orderByField": labelOrderFieldIssueCount,
209224
"orderByDirection": githubv4.OrderDirectionDesc,
210225
}
226+
if paginationParams.After != nil {
227+
vars["after"] = githubv4.String(*paginationParams.After)
228+
} else {
229+
vars["after"] = (*githubv4.String)(nil)
230+
}
211231

212-
labels := make([]map[string]any, 0)
213-
hasMore := false
214-
for page := 1; ; page++ {
215-
if err := client.Query(ctx, &query, vars); err != nil {
216-
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to list labels", err), nil, nil
217-
}
218-
for _, labelNode := range query.Repository.Labels.Nodes {
219-
labels = append(labels, map[string]any{
220-
"id": fmt.Sprintf("%v", labelNode.ID),
221-
"name": string(labelNode.Name),
222-
"color": string(labelNode.Color),
223-
"description": string(labelNode.Description),
224-
})
225-
}
226-
if !query.Repository.Labels.PageInfo.HasNextPage {
227-
break
228-
}
229-
if page >= listLabelsMaxPages {
230-
hasMore = true
231-
break
232+
if err := client.Query(ctx, &query, vars); err != nil {
233+
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to list labels", err), nil, nil
234+
}
235+
236+
labels := make([]map[string]any, len(query.Repository.Labels.Nodes))
237+
for i, labelNode := range query.Repository.Labels.Nodes {
238+
labels[i] = map[string]any{
239+
"id": fmt.Sprintf("%v", labelNode.ID),
240+
"name": string(labelNode.Name),
241+
"color": string(labelNode.Color),
242+
"description": string(labelNode.Description),
232243
}
233-
vars["cursor"] = githubv4.NewString(query.Repository.Labels.PageInfo.EndCursor)
234244
}
235245

246+
labelsPage := query.Repository.Labels.PageInfo
236247
response := map[string]any{
237248
"labels": labels,
238249
"totalCount": int(query.Repository.Labels.TotalCount),
239-
"has_more": hasMore,
250+
"pageInfo": MinimalPageInfo{
251+
HasNextPage: bool(labelsPage.HasNextPage),
252+
HasPreviousPage: bool(labelsPage.HasPreviousPage),
253+
StartCursor: string(labelsPage.StartCursor),
254+
EndCursor: string(labelsPage.EndCursor),
255+
},
240256
}
241257

242258
out, err := json.Marshal(response)

0 commit comments

Comments
 (0)