Skip to content

Commit 7272ef3

Browse files
committed
Add list_code_quality_findings tool to code_quality toolset
Add a read-only tool that lists code quality findings for a repository via GET /repos/{owner}/{repo}/code-quality/findings. It supports the endpoint's filters: state (open or dismissed, defaulting to open), direction (asc or desc), and cursor pagination (perPage, after). The tool follows the list_dependabot_alerts pattern: cursor pagination schema, defaults stripped from the validation schema, a typed output with findings and pageInfo, and the same scopes as get_code_quality_finding. Like get_code_quality_finding, it attaches no IFC label. Includes unit tests, the tool snapshot, and regenerated README docs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent f385262 commit 7272ef3

9 files changed

Lines changed: 655 additions & 17 deletions

‎README.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,15 @@ For negotiated supported protocol versions `2026-07-28` or newer, the consolidat
704704
- `owner`: The owner of the repository. (string, required)
705705
- `repo`: The name of the repository. (string, required)
706706

707+
- **list_code_quality_findings** - List code quality findings
708+
- **OAuth Challenge Scopes**: `repo`
709+
- `after`: Cursor for pagination. Use the cursor from the previous response. (string, optional)
710+
- `direction`: The direction to sort the results by. Defaults to desc (string, optional)
711+
- `owner`: The owner of the repository. (string, required)
712+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
713+
- `repo`: The name of the repository. (string, required)
714+
- `state`: Filter code quality findings by state. Defaults to open (string, optional)
715+
707716
</details>
708717

709718
<details>
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
{
2+
"annotations": {
3+
"idempotentHint": false,
4+
"readOnlyHint": true,
5+
"title": "List code quality findings"
6+
},
7+
"description": "List code quality findings in a GitHub repository.",
8+
"inputSchema": {
9+
"properties": {
10+
"after": {
11+
"description": "Cursor for pagination. Use the cursor from the previous response.",
12+
"type": "string"
13+
},
14+
"direction": {
15+
"description": "The direction to sort the results by. Defaults to desc",
16+
"enum": [
17+
"asc",
18+
"desc"
19+
],
20+
"type": "string"
21+
},
22+
"owner": {
23+
"description": "The owner of the repository.",
24+
"type": "string"
25+
},
26+
"perPage": {
27+
"description": "Results per page for pagination (min 1, max 100)",
28+
"maximum": 100,
29+
"minimum": 1,
30+
"type": "number"
31+
},
32+
"repo": {
33+
"description": "The name of the repository.",
34+
"type": "string"
35+
},
36+
"state": {
37+
"default": "open",
38+
"description": "Filter code quality findings by state. Defaults to open",
39+
"enum": [
40+
"open",
41+
"dismissed"
42+
],
43+
"type": "string"
44+
}
45+
},
46+
"required": [
47+
"owner",
48+
"repo"
49+
],
50+
"type": "object"
51+
},
52+
"name": "list_code_quality_findings",
53+
"outputSchema": {
54+
"additionalProperties": false,
55+
"properties": {
56+
"findings": {
57+
"items": {
58+
"additionalProperties": false,
59+
"properties": {
60+
"created_at": {
61+
"description": "Finding creation time (RFC 3339).",
62+
"type": [
63+
"null",
64+
"string"
65+
]
66+
},
67+
"location": {
68+
"additionalProperties": false,
69+
"properties": {
70+
"end_column": {
71+
"type": [
72+
"null",
73+
"integer"
74+
]
75+
},
76+
"end_line": {
77+
"type": [
78+
"null",
79+
"integer"
80+
]
81+
},
82+
"path": {
83+
"type": [
84+
"null",
85+
"string"
86+
]
87+
},
88+
"start_column": {
89+
"type": [
90+
"null",
91+
"integer"
92+
]
93+
},
94+
"start_line": {
95+
"type": [
96+
"null",
97+
"integer"
98+
]
99+
}
100+
},
101+
"type": [
102+
"null",
103+
"object"
104+
]
105+
},
106+
"message": {
107+
"additionalProperties": false,
108+
"properties": {
109+
"markdown": {
110+
"type": "string"
111+
},
112+
"text": {
113+
"type": "string"
114+
}
115+
},
116+
"required": [
117+
"text",
118+
"markdown"
119+
],
120+
"type": [
121+
"null",
122+
"object"
123+
]
124+
},
125+
"number": {
126+
"type": [
127+
"null",
128+
"integer"
129+
]
130+
},
131+
"rule": {
132+
"additionalProperties": false,
133+
"properties": {
134+
"category": {
135+
"type": [
136+
"null",
137+
"string"
138+
]
139+
},
140+
"description": {
141+
"type": [
142+
"null",
143+
"string"
144+
]
145+
},
146+
"help": {
147+
"type": [
148+
"null",
149+
"string"
150+
]
151+
},
152+
"id": {
153+
"type": [
154+
"null",
155+
"string"
156+
]
157+
},
158+
"severity": {
159+
"type": [
160+
"null",
161+
"string"
162+
]
163+
},
164+
"title": {
165+
"type": [
166+
"null",
167+
"string"
168+
]
169+
}
170+
},
171+
"type": [
172+
"null",
173+
"object"
174+
]
175+
},
176+
"state": {
177+
"type": [
178+
"null",
179+
"string"
180+
]
181+
}
182+
},
183+
"type": [
184+
"null",
185+
"object"
186+
]
187+
},
188+
"type": [
189+
"null",
190+
"array"
191+
]
192+
},
193+
"pageInfo": {
194+
"additionalProperties": false,
195+
"properties": {
196+
"hasNextPage": {
197+
"type": "boolean"
198+
},
199+
"hasPreviousPage": {
200+
"type": "boolean"
201+
},
202+
"nextCursor": {
203+
"type": "string"
204+
},
205+
"prevCursor": {
206+
"type": "string"
207+
}
208+
},
209+
"required": [
210+
"hasNextPage",
211+
"hasPreviousPage"
212+
],
213+
"type": "object"
214+
}
215+
},
216+
"required": [
217+
"findings",
218+
"pageInfo"
219+
],
220+
"type": "object"
221+
}
222+
}

‎pkg/github/code_quality.go‎

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"fmt"
77
"io"
88
"net/http"
9+
"net/url"
10+
"strconv"
911

1012
"github.com/google/jsonschema-go/jsonschema"
1113
"github.com/modelcontextprotocol/go-sdk/mcp"
@@ -99,6 +101,133 @@ func GetCodeQualityFinding(t translations.TranslationHelperFunc) inventory.Serve
99101
)
100102
}
101103

104+
func ListCodeQualityFindings(t translations.TranslationHelperFunc) inventory.ServerTool {
105+
schema := &jsonschema.Schema{
106+
Type: "object",
107+
Properties: map[string]*jsonschema.Schema{
108+
"owner": {
109+
Type: "string",
110+
Description: "The owner of the repository.",
111+
},
112+
"repo": {
113+
Type: "string",
114+
Description: "The name of the repository.",
115+
},
116+
"state": {
117+
Type: "string",
118+
Description: "Filter code quality findings by state. Defaults to open",
119+
Enum: []any{"open", "dismissed"},
120+
Default: json.RawMessage(`"open"`),
121+
},
122+
"direction": {
123+
Type: "string",
124+
Description: "The direction to sort the results by. Defaults to desc",
125+
Enum: []any{"asc", "desc"},
126+
},
127+
},
128+
Required: []string{"owner", "repo"},
129+
}
130+
WithCursorPagination(schema)
131+
132+
return NewToolWithSchemaOptions[ListCodeQualityFindingsInput, CodeQualityFindingsOutput](
133+
ToolsetMetadataCodeQuality,
134+
mcp.Tool{
135+
Name: "list_code_quality_findings",
136+
Description: t("TOOL_LIST_CODE_QUALITY_FINDINGS_DESCRIPTION", "List code quality findings in a GitHub repository."),
137+
Annotations: &mcp.ToolAnnotations{
138+
Title: t("TOOL_LIST_CODE_QUALITY_FINDINGS_USER_TITLE", "List code quality findings"),
139+
ReadOnlyHint: true,
140+
},
141+
InputSchema: schema,
142+
},
143+
scopes.PublicRead(scopes.Repo),
144+
inventory.TypedSchemaOptions{
145+
ValidationInputSchema: inventory.CloneSchemaWithoutDefaults(schema),
146+
},
147+
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args ListCodeQualityFindingsInput) (*mcp.CallToolResult, CodeQualityFindingsOutput, error) {
148+
if args.Owner == "" {
149+
return utils.NewToolResultError("missing required parameter: owner"), CodeQualityFindingsOutput{}, nil
150+
}
151+
if args.Repo == "" {
152+
return utils.NewToolResultError("missing required parameter: repo"), CodeQualityFindingsOutput{}, nil
153+
}
154+
155+
pagination := securityCursorPagination(args.PerPage, args.After)
156+
client, err := deps.GetClient(ctx)
157+
if err != nil {
158+
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), CodeQualityFindingsOutput{}, nil
159+
}
160+
161+
query := url.Values{}
162+
if args.State != "" {
163+
query.Set("state", args.State)
164+
}
165+
if args.Direction != "" {
166+
query.Set("direction", args.Direction)
167+
}
168+
query.Set("per_page", strconv.Itoa(pagination.PerPage))
169+
if pagination.After != "" {
170+
query.Set("after", pagination.After)
171+
}
172+
apiURL := fmt.Sprintf("repos/%s/%s/code-quality/findings?%s", url.PathEscape(args.Owner), url.PathEscape(args.Repo), query.Encode())
173+
req, err := client.NewRequest(ctx, http.MethodGet, apiURL, nil)
174+
if err != nil {
175+
return utils.NewToolResultErrorFromErr("failed to create request", err), CodeQualityFindingsOutput{}, nil
176+
}
177+
178+
var rawFindings json.RawMessage
179+
resp, err := client.Do(req, &rawFindings)
180+
if resp != nil {
181+
defer func() { _ = resp.Body.Close() }()
182+
}
183+
if err != nil {
184+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
185+
fmt.Sprintf("failed to list findings for repository '%s/%s'", args.Owner, args.Repo),
186+
resp,
187+
err,
188+
), CodeQualityFindingsOutput{}, nil
189+
}
190+
191+
if resp.StatusCode != http.StatusOK {
192+
body, err := io.ReadAll(resp.Body)
193+
if err != nil {
194+
return utils.NewToolResultErrorFromErr("failed to read response body", err), CodeQualityFindingsOutput{}, nil
195+
}
196+
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to list findings", resp, body), CodeQualityFindingsOutput{}, nil
197+
}
198+
199+
if len(rawFindings) == 0 {
200+
rawFindings = json.RawMessage("null")
201+
}
202+
var findings []*CodeQualityFindingOutput
203+
if err := json.Unmarshal(rawFindings, &findings); err != nil {
204+
return utils.NewToolResultErrorFromErr("failed to decode findings", err), CodeQualityFindingsOutput{}, nil
205+
}
206+
207+
apiPageInfo := buildPageInfo(resp)
208+
output := CodeQualityFindingsOutput{
209+
Findings: findings,
210+
PageInfo: SecurityPageInfo(apiPageInfo),
211+
}
212+
213+
response := struct {
214+
Findings json.RawMessage `json:"findings"`
215+
PageInfo pageInfo `json:"pageInfo"`
216+
}{
217+
Findings: rawFindings,
218+
PageInfo: apiPageInfo,
219+
}
220+
r, err := json.Marshal(response)
221+
if err != nil {
222+
return utils.NewToolResultErrorFromErr("failed to marshal findings", err), CodeQualityFindingsOutput{}, nil
223+
}
224+
225+
return utils.NewToolResultText(string(r)), output, nil
226+
},
227+
normalizeTypedReadArguments(nil, false),
228+
)
229+
}
230+
102231
func marshalLegacyCodeQualityFinding(rawFinding json.RawMessage) ([]byte, error) {
103232
// Match main's map-based formatting, including key ordering and number encoding.
104233
var finding map[string]any

0 commit comments

Comments
 (0)