Skip to content

Commit 79f584d

Browse files
feat(repos): add typed commit tool outputs
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent b9c2a32 commit 79f584d

3 files changed

Lines changed: 203 additions & 79 deletions

File tree

‎pkg/github/repositories.go‎

Lines changed: 75 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,17 @@ import (
2626
"github.com/shurcooL/githubv4"
2727
)
2828

29+
type GetCommitInput struct {
30+
Owner string `json:"owner"`
31+
Repo string `json:"repo"`
32+
SHA string `json:"sha"`
33+
Detail string `json:"detail,omitempty"`
34+
Page *int `json:"page,omitempty"`
35+
PerPage *int `json:"perPage,omitempty"`
36+
}
37+
2938
func GetCommit(t translations.TranslationHelperFunc) inventory.ServerTool {
30-
return NewTool(
39+
return NewTool[GetCommitInput, MinimalCommit](
3140
ToolsetMetadataRepos,
3241
mcp.Tool{
3342
Name: "get_commit",
@@ -62,30 +71,26 @@ func GetCommit(t translations.TranslationHelperFunc) inventory.ServerTool {
6271
}),
6372
},
6473
scopes.PublicRead(scopes.Repo),
65-
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
66-
owner, err := RequiredParam[string](args, "owner")
67-
if err != nil {
68-
return utils.NewToolResultError(err.Error()), nil, nil
74+
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, input GetCommitInput) (*mcp.CallToolResult, MinimalCommit, error) {
75+
if input.Owner == "" {
76+
return utils.NewToolResultError("missing required parameter: owner"), MinimalCommit{}, nil
6977
}
70-
repo, err := RequiredParam[string](args, "repo")
71-
if err != nil {
72-
return utils.NewToolResultError(err.Error()), nil, nil
78+
if input.Repo == "" {
79+
return utils.NewToolResultError("missing required parameter: repo"), MinimalCommit{}, nil
7380
}
74-
sha, err := RequiredParam[string](args, "sha")
75-
if err != nil {
76-
return utils.NewToolResultError(err.Error()), nil, nil
81+
if input.SHA == "" {
82+
return utils.NewToolResultError("missing required parameter: sha"), MinimalCommit{}, nil
7783
}
78-
detailRaw, err := OptionalParam[string](args, "detail")
84+
detail, err := parseCommitDetail(input.Detail)
7985
if err != nil {
80-
return utils.NewToolResultError(err.Error()), nil, nil
86+
return utils.NewToolResultError(err.Error()), MinimalCommit{}, nil
8187
}
82-
detail, err := parseCommitDetail(detailRaw)
83-
if err != nil {
84-
return utils.NewToolResultError(err.Error()), nil, nil
88+
pagination := PaginationParams{Page: 1, PerPage: 30}
89+
if input.Page != nil {
90+
pagination.Page = *input.Page
8591
}
86-
pagination, err := OptionalPaginationParams(args)
87-
if err != nil {
88-
return utils.NewToolResultError(err.Error()), nil, nil
92+
if input.PerPage != nil {
93+
pagination.PerPage = *input.PerPage
8994
}
9095

9196
opts := &github.ListOptions{
@@ -95,47 +100,61 @@ func GetCommit(t translations.TranslationHelperFunc) inventory.ServerTool {
95100

96101
client, err := deps.GetClient(ctx)
97102
if err != nil {
98-
return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
103+
return nil, MinimalCommit{}, fmt.Errorf("failed to get GitHub client: %w", err)
99104
}
100-
commit, resp, err := client.Repositories.GetCommit(ctx, owner, repo, sha, opts)
105+
commit, resp, err := client.Repositories.GetCommit(ctx, input.Owner, input.Repo, input.SHA, opts)
101106
if err != nil {
102107
return ghErrors.NewGitHubAPIErrorResponse(ctx,
103-
fmt.Sprintf("failed to get commit: %s", sha),
108+
fmt.Sprintf("failed to get commit: %s", input.SHA),
104109
resp,
105110
err,
106-
), nil, nil
111+
), MinimalCommit{}, nil
107112
}
108113
defer func() { _ = resp.Body.Close() }()
109114

110115
if resp.StatusCode != 200 {
111116
body, err := io.ReadAll(resp.Body)
112117
if err != nil {
113-
return nil, nil, fmt.Errorf("failed to read response body: %w", err)
118+
return nil, MinimalCommit{}, fmt.Errorf("failed to read response body: %w", err)
114119
}
115-
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get commit", resp, body), nil, nil
120+
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get commit", resp, body), MinimalCommit{}, nil
116121
}
117122

118123
// Convert to minimal commit
119124
minimalCommit := convertToMinimalCommit(commit, detail)
120125

121126
r, err := json.Marshal(minimalCommit)
122127
if err != nil {
123-
return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
128+
return nil, MinimalCommit{}, fmt.Errorf("failed to marshal response: %w", err)
124129
}
125130

126131
result := utils.NewToolResultText(string(r))
127132
// Commit content is reachable from the repo's history; in public
128133
// repos anyone can land it via a PR (untrusted), in private repos
129134
// only collaborators can (trusted). Confidentiality follows repo
130135
// visibility.
131-
result = attachRepoVisibilityIFCLabel(ctx, deps, client, owner, repo, result, ifc.LabelCommitContents)
132-
return result, nil, nil
136+
result = attachRepoVisibilityIFCLabel(ctx, deps, client, input.Owner, input.Repo, result, ifc.LabelCommitContents)
137+
return result, minimalCommit, nil
133138
},
139+
normalizeTypedReadArgumentsPreservingZero(nil),
134140
)
135141
}
136142

137143
// ListCommits creates a tool to get the list of commits of a branch in a GitHub
138144
// repository.
145+
type ListCommitsInput struct {
146+
Owner string `json:"owner"`
147+
Repo string `json:"repo"`
148+
SHA string `json:"sha,omitempty"`
149+
Author string `json:"author,omitempty"`
150+
Path string `json:"path,omitempty"`
151+
Since string `json:"since,omitempty"`
152+
Until string `json:"until,omitempty"`
153+
Fields []string `json:"fields,omitempty"`
154+
Page *int `json:"page,omitempty"`
155+
PerPage *int `json:"perPage,omitempty"`
156+
}
157+
139158
func ListCommits(t translations.TranslationHelperFunc) inventory.ServerTool {
140159
schema := &jsonschema.Schema{
141160
Type: "object",
@@ -177,7 +196,7 @@ func ListCommits(t translations.TranslationHelperFunc) inventory.ServerTool {
177196
)
178197
WithPagination(schema)
179198

180-
return NewTool(
199+
return NewTool[ListCommitsInput, []MinimalCommit](
181200
ToolsetMetadataRepos,
182201
mcp.Tool{
183202
Name: "list_commits",
@@ -189,66 +208,43 @@ func ListCommits(t translations.TranslationHelperFunc) inventory.ServerTool {
189208
InputSchema: schema,
190209
},
191210
scopes.PublicRead(scopes.Repo),
192-
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
193-
owner, err := RequiredParam[string](args, "owner")
194-
if err != nil {
195-
return utils.NewToolResultError(err.Error()), nil, nil
196-
}
197-
repo, err := RequiredParam[string](args, "repo")
198-
if err != nil {
199-
return utils.NewToolResultError(err.Error()), nil, nil
200-
}
201-
sha, err := OptionalParam[string](args, "sha")
202-
if err != nil {
203-
return utils.NewToolResultError(err.Error()), nil, nil
204-
}
205-
author, err := OptionalParam[string](args, "author")
206-
if err != nil {
207-
return utils.NewToolResultError(err.Error()), nil, nil
208-
}
209-
path, err := OptionalParam[string](args, "path")
210-
if err != nil {
211-
return utils.NewToolResultError(err.Error()), nil, nil
212-
}
213-
fields, err := OptionalStringArrayParam(args, "fields")
214-
if err != nil {
215-
return utils.NewToolResultError(err.Error()), nil, nil
211+
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, input ListCommitsInput) (*mcp.CallToolResult, []MinimalCommit, error) {
212+
if input.Owner == "" {
213+
return utils.NewToolResultError("missing required parameter: owner"), nil, nil
216214
}
217-
sinceStr, err := OptionalParam[string](args, "since")
218-
if err != nil {
219-
return utils.NewToolResultError(err.Error()), nil, nil
215+
if input.Repo == "" {
216+
return utils.NewToolResultError("missing required parameter: repo"), nil, nil
220217
}
221-
untilStr, err := OptionalParam[string](args, "until")
222-
if err != nil {
223-
return utils.NewToolResultError(err.Error()), nil, nil
218+
pagination := PaginationParams{Page: 1, PerPage: 30}
219+
if input.Page != nil {
220+
pagination.Page = *input.Page
224221
}
225-
pagination, err := OptionalPaginationParams(args)
226-
if err != nil {
227-
return utils.NewToolResultError(err.Error()), nil, nil
222+
if input.PerPage != nil {
223+
pagination.PerPage = *input.PerPage
228224
}
229225
// Set default perPage to 30 if not provided
230226
perPage := pagination.PerPage
231227
if perPage == 0 {
232228
perPage = 30
233229
}
234230
opts := &github.CommitsListOptions{
235-
SHA: sha,
236-
Path: path,
237-
Author: author,
231+
SHA: input.SHA,
232+
Path: input.Path,
233+
Author: input.Author,
238234
ListOptions: github.ListOptions{
239235
Page: pagination.Page,
240236
PerPage: perPage,
241237
},
242238
}
243-
if sinceStr != "" {
244-
sinceTime, err := parseISOTimestamp(sinceStr)
239+
if input.Since != "" {
240+
sinceTime, err := parseISOTimestamp(input.Since)
245241
if err != nil {
246242
return utils.NewToolResultError(fmt.Sprintf("invalid since timestamp: %s", err)), nil, nil
247243
}
248244
opts.Since = sinceTime
249245
}
250-
if untilStr != "" {
251-
untilTime, err := parseISOTimestamp(untilStr)
246+
if input.Until != "" {
247+
untilTime, err := parseISOTimestamp(input.Until)
252248
if err != nil {
253249
return utils.NewToolResultError(fmt.Sprintf("invalid until timestamp: %s", err)), nil, nil
254250
}
@@ -259,10 +255,10 @@ func ListCommits(t translations.TranslationHelperFunc) inventory.ServerTool {
259255
if err != nil {
260256
return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
261257
}
262-
commits, resp, err := client.Repositories.ListCommits(ctx, owner, repo, opts)
258+
commits, resp, err := client.Repositories.ListCommits(ctx, input.Owner, input.Repo, opts)
263259
if err != nil {
264260
return ghErrors.NewGitHubAPIErrorResponse(ctx,
265-
fmt.Sprintf("failed to list commits: %s", sha),
261+
fmt.Sprintf("failed to list commits: %s", input.SHA),
266262
resp,
267263
err,
268264
), nil, nil
@@ -285,8 +281,8 @@ func ListCommits(t translations.TranslationHelperFunc) inventory.ServerTool {
285281

286282
filtered := false
287283
var payload any = minimalCommits
288-
if len(fields) > 0 {
289-
filteredCommits, err := filterEachField(minimalCommits, fields)
284+
if len(input.Fields) > 0 {
285+
filteredCommits, err := filterEachField(minimalCommits, input.Fields)
290286
if err != nil {
291287
return utils.NewToolResultErrorFromErr("failed to filter commits", err), nil, nil
292288
}
@@ -305,9 +301,13 @@ func ListCommits(t translations.TranslationHelperFunc) inventory.ServerTool {
305301
// Commit content is reachable from the repo's history; integrity
306302
// follows the same public-untrusted / private-trusted rule as file
307303
// contents. Confidentiality follows repo visibility.
308-
result = attachRepoVisibilityIFCLabel(ctx, deps, client, owner, repo, result, ifc.LabelCommitContents)
309-
return result, nil, nil
304+
result = attachRepoVisibilityIFCLabel(ctx, deps, client, input.Owner, input.Repo, result, ifc.LabelCommitContents)
305+
if len(input.Fields) > 0 {
306+
return result, nil, nil
307+
}
308+
return result, minimalCommits, nil
310309
},
310+
normalizeTypedReadArgumentsPreservingZero(nil),
311311
)
312312
}
313313

‎pkg/github/typed_read_normalizers.go‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ import (
1111
)
1212

1313
func normalizeTypedReadArguments(uppercaseFields []string, preserveZeroPage bool) inventory.InputNormalizer {
14+
return normalizeTypedArguments(uppercaseFields, preserveZeroPage, false)
15+
}
16+
17+
func normalizeTypedReadArgumentsPreservingZero(uppercaseFields []string) inventory.InputNormalizer {
18+
return normalizeTypedArguments(uppercaseFields, true, true)
19+
}
20+
21+
func normalizeTypedArguments(uppercaseFields []string, preserveZeroPage, preserveZeroPerPage bool) inventory.InputNormalizer {
1422
return func(raw json.RawMessage) (json.RawMessage, error) {
1523
var args map[string]json.RawMessage
1624
if err := json.Unmarshal(raw, &args); err != nil {
@@ -36,9 +44,6 @@ func normalizeTypedReadArguments(uppercaseFields []string, preserveZeroPage bool
3644
if !exists {
3745
continue
3846
}
39-
if field == "page" && preserveZeroPage {
40-
continue
41-
}
4247
var number any
4348
if err := json.Unmarshal(value, &number); err != nil {
4449
return nil, err
@@ -60,7 +65,7 @@ func normalizeTypedReadArguments(uppercaseFields []string, preserveZeroPage bool
6065
continue
6166
}
6267
}
63-
if field == "perPage" {
68+
if field == "perPage" && !preserveZeroPerPage {
6469
if parsed, ok := number.(float64); ok && parsed == 0 {
6570
delete(args, field)
6671
continue

0 commit comments

Comments
 (0)