Skip to content

Commit acf6dbc

Browse files
committed
refactor approach
1 parent b1c3b16 commit acf6dbc

11 files changed

Lines changed: 307 additions & 189 deletions

pkg/github/context_tools.go

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,7 @@ func GetMe(t translations.TranslationHelperFunc) inventory.ServerTool {
5454
},
5555
// Use json.RawMessage to ensure "properties" is included even when empty.
5656
// OpenAI strict mode requires the properties field to be present.
57-
InputSchema: json.RawMessage(`{"type":"object","properties":{}}`),
58-
OutputSchema: MustOutputSchema[MinimalUser](),
57+
InputSchema: json.RawMessage(`{"type":"object","properties":{}}`),
5958
Meta: mcp.Meta{
6059
"ui": map[string]any{
6160
"resourceUri": GetMeUIResourceURI,
@@ -105,7 +104,10 @@ func GetMe(t translations.TranslationHelperFunc) inventory.ServerTool {
105104
},
106105
}
107106

108-
result := MarshalledTextResult(minimalUser)
107+
result, err := structuredTextResult(ctx, deps, minimalUser, minimalUser)
108+
if err != nil {
109+
return nil, nil, err
110+
}
109111
if deps.GetFlags(ctx).InsidersMode {
110112
if result.Meta == nil {
111113
result.Meta = mcp.Meta{}
@@ -114,7 +116,7 @@ func GetMe(t translations.TranslationHelperFunc) inventory.ServerTool {
114116
}
115117
return result, nil, nil
116118
},
117-
)
119+
).WithOutputSchema(MustOutputSchema[MinimalUser]())
118120
}
119121

120122
type TeamInfo struct {
@@ -128,6 +130,14 @@ type OrganizationTeams struct {
128130
Teams []TeamInfo `json:"teams"`
129131
}
130132

133+
type TeamsResponse struct {
134+
Teams []OrganizationTeams `json:"teams"`
135+
}
136+
137+
type TeamMembersResponse struct {
138+
Members []string `json:"members"`
139+
}
140+
131141
func GetTeams(t translations.TranslationHelperFunc) inventory.ServerTool {
132142
return NewTool(
133143
ToolsetMetadataContext,
@@ -221,9 +231,13 @@ func GetTeams(t translations.TranslationHelperFunc) inventory.ServerTool {
221231
organizations = append(organizations, orgTeams)
222232
}
223233

224-
return MarshalledTextResult(organizations), nil, nil
234+
result, err := structuredTextResult(ctx, deps, organizations, TeamsResponse{Teams: organizations})
235+
if err != nil {
236+
return nil, nil, err
237+
}
238+
return result, nil, nil
225239
},
226-
)
240+
).WithOutputSchema(MustOutputSchema[TeamsResponse]())
227241
}
228242

229243
func GetTeamMembers(t translations.TranslationHelperFunc) inventory.ServerTool {
@@ -292,7 +306,11 @@ func GetTeamMembers(t translations.TranslationHelperFunc) inventory.ServerTool {
292306
members = append(members, string(member.Login))
293307
}
294308

295-
return MarshalledTextResult(members), nil, nil
309+
result, err := structuredTextResult(ctx, deps, members, TeamMembersResponse{Members: members})
310+
if err != nil {
311+
return nil, nil, err
312+
}
313+
return result, nil, nil
296314
},
297-
)
315+
).WithOutputSchema(MustOutputSchema[TeamMembersResponse]())
298316
}

pkg/github/issues.go

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -593,13 +593,20 @@ func ListIssueTypes(t translations.TranslationHelperFunc) inventory.ServerTool {
593593
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to list issue types", resp, body), nil, nil
594594
}
595595

596-
r, err := json.Marshal(issueTypes)
596+
minimalIssueTypes := make([]MinimalIssueType, 0, len(issueTypes))
597+
for _, issueType := range issueTypes {
598+
if issueType != nil {
599+
minimalIssueTypes = append(minimalIssueTypes, convertToMinimalIssueType(issueType))
600+
}
601+
}
602+
603+
result, err := structuredTextResult(ctx, deps, issueTypes, MinimalIssueTypesResponse{IssueTypes: minimalIssueTypes})
597604
if err != nil {
598605
return utils.NewToolResultErrorFromErr("failed to marshal issue types", err), nil, nil
599606
}
600607

601-
return utils.NewToolResultText(string(r)), nil, nil
602-
})
608+
return result, nil, nil
609+
}).WithOutputSchema(MustOutputSchema[MinimalIssueTypesResponse]())
603610
}
604611

605612
// AddIssueComment creates a tool to add a comment to an issue.
@@ -978,9 +985,9 @@ func SearchIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
978985
},
979986
[]scopes.Scope{scopes.Repo},
980987
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
981-
result, err := searchHandler(ctx, deps.GetClient, args, "issue", "failed to search issues")
988+
result, err := searchHandler(ctx, deps, args, "issue", "failed to search issues")
982989
return result, nil, err
983-
})
990+
}).WithOutputSchema(MustOutputSchema[MinimalSearchIssuesResult]())
984991
}
985992

986993
// IssueWrite creates a tool to create a new or update an existing issue in a GitHub repository.
@@ -1432,8 +1439,7 @@ func ListIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
14321439
Title: t("TOOL_LIST_ISSUES_USER_TITLE", "List issues"),
14331440
ReadOnlyHint: true,
14341441
},
1435-
InputSchema: schema,
1436-
OutputSchema: MustOutputSchema[MinimalIssuesResponse](),
1442+
InputSchema: schema,
14371443
},
14381444
[]scopes.Scope{scopes.Repo},
14391445
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
@@ -1591,7 +1597,10 @@ func ListIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
15911597
isPrivate = queryResult.GetIsPrivate()
15921598
}
15931599

1594-
result := MarshalledTextResult(resp)
1600+
result, err := structuredTextResult(ctx, deps, resp, resp)
1601+
if err != nil {
1602+
return nil, nil, err
1603+
}
15951604
if deps.GetFlags(ctx).InsidersMode {
15961605
if result.Meta == nil {
15971606
result.Meta = mcp.Meta{}
@@ -1614,7 +1623,7 @@ func ListIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
16141623
result.Meta["ifc"] = ifc.LabelListIssues(isPrivate, readers)
16151624
}
16161625
return result, nil, nil
1617-
})
1626+
}).WithOutputSchema(MustOutputSchema[MinimalIssuesResponse]())
16181627
}
16191628

16201629
// parseISOTimestamp parses an ISO 8601 timestamp string into a time.Time object.

pkg/github/labels.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,21 +93,21 @@ func GetLabel(t translations.TranslationHelperFunc) inventory.ServerTool {
9393
return utils.NewToolResultError(fmt.Sprintf("label '%s' not found in %s/%s", name, owner, repo)), nil, nil
9494
}
9595

96-
label := map[string]any{
97-
"id": fmt.Sprintf("%v", query.Repository.Label.ID),
98-
"name": string(query.Repository.Label.Name),
99-
"color": string(query.Repository.Label.Color),
100-
"description": string(query.Repository.Label.Description),
96+
label := MinimalLabel{
97+
ID: fmt.Sprintf("%v", query.Repository.Label.ID),
98+
Name: string(query.Repository.Label.Name),
99+
Color: string(query.Repository.Label.Color),
100+
Description: string(query.Repository.Label.Description),
101101
}
102102

103-
out, err := json.Marshal(label)
103+
result, err := structuredTextResult(ctx, deps, label, label)
104104
if err != nil {
105105
return nil, nil, fmt.Errorf("failed to marshal label: %w", err)
106106
}
107107

108-
return utils.NewToolResultText(string(out)), nil, nil
108+
return result, nil, nil
109109
},
110-
)
110+
).WithOutputSchema(MustOutputSchema[MinimalLabel]())
111111
}
112112

113113
// GetLabelForLabelsToolset returns the same GetLabel tool but registered in the labels toolset.

pkg/github/minimal_types.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,22 @@ type MinimalSearchRepositoriesResult struct {
5151
Items []MinimalRepository `json:"items"`
5252
}
5353

54+
// MinimalCodeSearchResult is the trimmed output type for code search results.
55+
type MinimalCodeSearchResult struct {
56+
TotalCount int `json:"total_count"`
57+
IncompleteResults bool `json:"incomplete_results"`
58+
Items []MinimalCodeSearchItem `json:"items"`
59+
}
60+
61+
// MinimalCodeSearchItem is the trimmed output type for code search result objects.
62+
type MinimalCodeSearchItem struct {
63+
Name string `json:"name,omitempty"`
64+
Path string `json:"path,omitempty"`
65+
SHA string `json:"sha,omitempty"`
66+
HTMLURL string `json:"html_url,omitempty"`
67+
Repository *MinimalRepository `json:"repository,omitempty"`
68+
}
69+
5470
// MinimalCommitAuthor represents commit author information.
5571
type MinimalCommitAuthor struct {
5672
Name string `json:"name,omitempty"`
@@ -104,6 +120,11 @@ type MinimalCommit struct {
104120
Files []MinimalCommitFile `json:"files,omitempty"`
105121
}
106122

123+
// MinimalCommitsResponse wraps commits for MCP structured output.
124+
type MinimalCommitsResponse struct {
125+
Commits []MinimalCommit `json:"commits"`
126+
}
127+
107128
// MinimalRelease is the trimmed output type for release objects.
108129
type MinimalRelease struct {
109130
ID int64 `json:"id"`
@@ -117,19 +138,58 @@ type MinimalRelease struct {
117138
Author *MinimalUser `json:"author,omitempty"`
118139
}
119140

141+
// MinimalReleasesResponse wraps releases for MCP structured output.
142+
type MinimalReleasesResponse struct {
143+
Releases []MinimalRelease `json:"releases"`
144+
}
145+
120146
// MinimalBranch is the trimmed output type for branch objects.
121147
type MinimalBranch struct {
122148
Name string `json:"name"`
123149
SHA string `json:"sha"`
124150
Protected bool `json:"protected"`
125151
}
126152

153+
// MinimalBranchesResponse wraps branches for MCP structured output.
154+
type MinimalBranchesResponse struct {
155+
Branches []MinimalBranch `json:"branches"`
156+
}
157+
127158
// MinimalTag is the trimmed output type for tag objects.
128159
type MinimalTag struct {
129160
Name string `json:"name"`
130161
SHA string `json:"sha"`
131162
}
132163

164+
// MinimalTagsResponse wraps tags for MCP structured output.
165+
type MinimalTagsResponse struct {
166+
Tags []MinimalTag `json:"tags"`
167+
}
168+
169+
// MinimalIssueType is the trimmed output type for issue type objects.
170+
type MinimalIssueType struct {
171+
ID int64 `json:"id,omitempty"`
172+
NodeID string `json:"node_id,omitempty"`
173+
Name string `json:"name,omitempty"`
174+
Description string `json:"description,omitempty"`
175+
Color string `json:"color,omitempty"`
176+
CreatedAt string `json:"created_at,omitempty"`
177+
UpdatedAt string `json:"updated_at,omitempty"`
178+
}
179+
180+
// MinimalIssueTypesResponse wraps issue types for MCP structured output.
181+
type MinimalIssueTypesResponse struct {
182+
IssueTypes []MinimalIssueType `json:"issue_types"`
183+
}
184+
185+
// MinimalLabel is the output type for repository labels.
186+
type MinimalLabel struct {
187+
ID string `json:"id"`
188+
Name string `json:"name"`
189+
Color string `json:"color,omitempty"`
190+
Description string `json:"description,omitempty"`
191+
}
192+
133193
// MinimalResponse represents a minimal response for all CRUD operations.
134194
// Success is implicit in the HTTP response status, and all other information
135195
// can be derived from the URL or fetched separately if needed.
@@ -200,6 +260,13 @@ type MinimalIssuesResponse struct {
200260
PageInfo MinimalPageInfo `json:"pageInfo"`
201261
}
202262

263+
// MinimalSearchIssuesResult is the trimmed output type for issue and pull request search results.
264+
type MinimalSearchIssuesResult struct {
265+
TotalCount int `json:"total_count"`
266+
IncompleteResults bool `json:"incomplete_results"`
267+
Items []MinimalIssue `json:"items"`
268+
}
269+
203270
// MinimalIssueComment is the trimmed output type for issue comment objects to reduce verbosity.
204271
type MinimalIssueComment struct {
205272
ID int64 `json:"id"`
@@ -622,6 +689,36 @@ func convertToMinimalUser(user *github.User) *MinimalUser {
622689
}
623690
}
624691

692+
func convertToMinimalRepository(repo *github.Repository) MinimalRepository {
693+
minimalRepo := MinimalRepository{
694+
ID: repo.GetID(),
695+
Name: repo.GetName(),
696+
FullName: repo.GetFullName(),
697+
Description: repo.GetDescription(),
698+
HTMLURL: repo.GetHTMLURL(),
699+
Language: repo.GetLanguage(),
700+
Stars: repo.GetStargazersCount(),
701+
Forks: repo.GetForksCount(),
702+
OpenIssues: repo.GetOpenIssuesCount(),
703+
Private: repo.GetPrivate(),
704+
Fork: repo.GetFork(),
705+
Archived: repo.GetArchived(),
706+
DefaultBranch: repo.GetDefaultBranch(),
707+
}
708+
709+
if repo.UpdatedAt != nil {
710+
minimalRepo.UpdatedAt = repo.UpdatedAt.Format(time.RFC3339)
711+
}
712+
if repo.CreatedAt != nil {
713+
minimalRepo.CreatedAt = repo.CreatedAt.Format(time.RFC3339)
714+
}
715+
if repo.Topics != nil {
716+
minimalRepo.Topics = repo.Topics
717+
}
718+
719+
return minimalRepo
720+
}
721+
625722
// convertToMinimalCommit converts a GitHub API RepositoryCommit to MinimalCommit
626723
func convertToMinimalCommit(commit *github.RepositoryCommit, includeDiffs bool) MinimalCommit {
627724
minimalCommit := MinimalCommit{
@@ -792,6 +889,70 @@ func convertToMinimalTag(tag *github.RepositoryTag) MinimalTag {
792889
return m
793890
}
794891

892+
func convertToMinimalIssueType(issueType *github.IssueType) MinimalIssueType {
893+
m := MinimalIssueType{
894+
ID: issueType.GetID(),
895+
NodeID: issueType.GetNodeID(),
896+
Name: issueType.GetName(),
897+
Description: issueType.GetDescription(),
898+
Color: issueType.GetColor(),
899+
}
900+
901+
if issueType.CreatedAt != nil {
902+
m.CreatedAt = issueType.CreatedAt.Format(time.RFC3339)
903+
}
904+
if issueType.UpdatedAt != nil {
905+
m.UpdatedAt = issueType.UpdatedAt.Format(time.RFC3339)
906+
}
907+
908+
return m
909+
}
910+
911+
func convertToMinimalCodeSearchResult(result *github.CodeSearchResult) MinimalCodeSearchResult {
912+
minimalResult := MinimalCodeSearchResult{
913+
TotalCount: result.GetTotal(),
914+
IncompleteResults: result.GetIncompleteResults(),
915+
Items: make([]MinimalCodeSearchItem, 0, len(result.CodeResults)),
916+
}
917+
918+
for _, item := range result.CodeResults {
919+
if item == nil {
920+
continue
921+
}
922+
923+
minimalItem := MinimalCodeSearchItem{
924+
Name: item.GetName(),
925+
Path: item.GetPath(),
926+
SHA: item.GetSHA(),
927+
HTMLURL: item.GetHTMLURL(),
928+
}
929+
if repo := item.GetRepository(); repo != nil {
930+
repository := convertToMinimalRepository(repo)
931+
minimalItem.Repository = &repository
932+
}
933+
934+
minimalResult.Items = append(minimalResult.Items, minimalItem)
935+
}
936+
937+
return minimalResult
938+
}
939+
940+
func convertToMinimalSearchIssuesResult(result *github.IssuesSearchResult) MinimalSearchIssuesResult {
941+
minimalResult := MinimalSearchIssuesResult{
942+
TotalCount: result.GetTotal(),
943+
IncompleteResults: result.GetIncompleteResults(),
944+
Items: make([]MinimalIssue, 0, len(result.Issues)),
945+
}
946+
947+
for _, issue := range result.Issues {
948+
if issue != nil {
949+
minimalResult.Items = append(minimalResult.Items, convertToMinimalIssue(issue))
950+
}
951+
}
952+
953+
return minimalResult
954+
}
955+
795956
// MinimalCheckRun is the trimmed output type for check run objects.
796957
type MinimalCheckRun struct {
797958
ID int64 `json:"id"`

0 commit comments

Comments
 (0)