Skip to content
Draft
1 change: 0 additions & 1 deletion internal/ghmcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,6 @@ func NewStdioMCPServer(ctx context.Context, cfg github.MCPServerConfig) (*mcp.Se
WithToolsets(github.ResolvedEnabledToolsets(cfg.DynamicToolsets, cfg.EnabledToolsets, cfg.EnabledTools)).
WithTools(github.CleanTools(cfg.EnabledTools)).
WithExcludeTools(cfg.ExcludeTools).
WithServerInstructions().
WithFeatureChecker(featureChecker)

// Apply token scope filtering if scopes are known (for PAT filtering)
Expand Down
8 changes: 4 additions & 4 deletions pkg/github/context_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,10 @@ func GetMe(t translations.TranslationHelperFunc) inventory.ServerTool {
},
},
nil,
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, _ map[string]any) (*mcp.CallToolResult, any, error) {
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, _ map[string]any) (*mcp.CallToolResult, MinimalUser, error) {
client, err := deps.GetClient(ctx)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), MinimalUser{}, nil
}

user, res, err := client.Users.Get(ctx, "")
Expand All @@ -73,7 +73,7 @@ func GetMe(t translations.TranslationHelperFunc) inventory.ServerTool {
"failed to get user",
res,
err,
), nil, nil
), MinimalUser{}, nil
}

// Create minimal user representation instead of returning full user object
Expand Down Expand Up @@ -103,7 +103,7 @@ func GetMe(t translations.TranslationHelperFunc) inventory.ServerTool {
},
}

return MarshalledTextResult(minimalUser), nil, nil
return MarshalledTextResult(minimalUser), minimalUser, nil
},
)
}
Expand Down
50 changes: 32 additions & 18 deletions pkg/github/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -961,9 +961,23 @@ func SearchIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
InputSchema: schema,
},
[]scopes.Scope{scopes.Repo},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
result, err := searchHandler(ctx, deps.GetClient, args, "issue", "failed to search issues")
return result, nil, err
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, IssueSearchResult, error) {
textResult, rawResult, err := searchHandler(ctx, deps.GetClient, args, "issue", "failed to search issues")
if rawResult == nil {
return textResult, IssueSearchResult{}, err
}
issues := make([]MinimalIssue, 0, len(rawResult.Issues))
for _, issue := range rawResult.Issues {
if issue != nil {
issues = append(issues, convertToMinimalIssue(issue))
}
}
structured := IssueSearchResult{
TotalCount: rawResult.GetTotal(),
IncompleteResults: rawResult.GetIncompleteResults(),
Items: issues,
}
return textResult, structured, nil
})
}

Expand Down Expand Up @@ -1419,20 +1433,20 @@ func ListIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
InputSchema: schema,
},
[]scopes.Scope{scopes.Repo},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, MinimalIssuesResponse, error) {
owner, err := RequiredParam[string](args, "owner")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
return utils.NewToolResultError(err.Error()), MinimalIssuesResponse{}, nil
}
repo, err := RequiredParam[string](args, "repo")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
return utils.NewToolResultError(err.Error()), MinimalIssuesResponse{}, nil
}

// Set optional parameters if provided
state, err := OptionalParam[string](args, "state")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
return utils.NewToolResultError(err.Error()), MinimalIssuesResponse{}, nil
}

// Normalize and filter by state
Expand All @@ -1449,17 +1463,17 @@ func ListIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
// Get labels
labels, err := OptionalStringArrayParam(args, "labels")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
return utils.NewToolResultError(err.Error()), MinimalIssuesResponse{}, nil
}

orderBy, err := OptionalParam[string](args, "orderBy")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
return utils.NewToolResultError(err.Error()), MinimalIssuesResponse{}, nil
}

direction, err := OptionalParam[string](args, "direction")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
return utils.NewToolResultError(err.Error()), MinimalIssuesResponse{}, nil
}

// Normalize and validate orderBy
Expand All @@ -1482,7 +1496,7 @@ func ListIssues(t translations.TranslationHelperFunc) inventory.ServerTool {

since, err := OptionalParam[string](args, "since")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
return utils.NewToolResultError(err.Error()), MinimalIssuesResponse{}, nil
}

// There are two optional parameters: since and labels.
Expand All @@ -1491,7 +1505,7 @@ func ListIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
if since != "" {
sinceTime, err = parseISOTimestamp(since)
if err != nil {
return utils.NewToolResultError(fmt.Sprintf("failed to list issues: %s", err.Error())), nil, nil
return utils.NewToolResultError(fmt.Sprintf("failed to list issues: %s", err.Error())), MinimalIssuesResponse{}, nil
}
hasSince = true
}
Expand All @@ -1500,12 +1514,12 @@ func ListIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
// Get pagination parameters and convert to GraphQL format
pagination, err := OptionalCursorPaginationParams(args)
if err != nil {
return nil, nil, err
return nil, MinimalIssuesResponse{}, err
}

// Check if someone tried to use page-based pagination instead of cursor-based
if _, pageProvided := args["page"]; pageProvided {
return utils.NewToolResultError("This tool uses cursor-based pagination. Use the 'after' parameter with the 'endCursor' value from the previous response instead of 'page'."), nil, nil
return utils.NewToolResultError("This tool uses cursor-based pagination. Use the 'after' parameter with the 'endCursor' value from the previous response instead of 'page'."), MinimalIssuesResponse{}, nil
}

// Check if pagination parameters were explicitly provided
Expand All @@ -1514,7 +1528,7 @@ func ListIssues(t translations.TranslationHelperFunc) inventory.ServerTool {

paginationParams, err := pagination.ToGraphQLParams()
if err != nil {
return nil, nil, err
return nil, MinimalIssuesResponse{}, err
}

// Use default of 30 if pagination was not explicitly provided
Expand All @@ -1525,7 +1539,7 @@ func ListIssues(t translations.TranslationHelperFunc) inventory.ServerTool {

client, err := deps.GetGQLClient(ctx)
if err != nil {
return utils.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil, nil
return utils.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), MinimalIssuesResponse{}, nil
}

vars := map[string]any{
Expand Down Expand Up @@ -1564,15 +1578,15 @@ func ListIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
ctx,
"failed to list issues",
err,
), nil, nil
), MinimalIssuesResponse{}, nil
}

var resp MinimalIssuesResponse
if queryResult, ok := issueQuery.(IssueQueryResult); ok {
resp = convertToMinimalIssuesResponse(queryResult.GetIssueFragment())
}

return MarshalledTextResult(resp), nil, nil
return MarshalledTextResult(resp), resp, nil
})
}

Expand Down
94 changes: 94 additions & 0 deletions pkg/github/minimal_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,58 @@ type MinimalPRBranchRepo struct {
Description string `json:"description,omitempty"`
}

// ListPullRequestsResult wraps the output of list_pull_requests for structured output.
type ListPullRequestsResult struct {
PullRequests []MinimalPullRequest `json:"pull_requests"`
}

// CodeSearchResult wraps the output of search_code for structured output.
type CodeSearchResult struct {
TotalCount int `json:"total_count"`
IncompleteResults bool `json:"incomplete_results"`
Items []CodeSearchResultItem `json:"items"`
}

// CodeSearchResultItem represents a single code search result item.
type CodeSearchResultItem struct {
Name string `json:"name"`
Path string `json:"path"`
SHA string `json:"sha"`
HTMLURL string `json:"html_url"`
Repository *MinimalRepository `json:"repository,omitempty"`
TextMatches []TextMatch `json:"text_matches,omitempty"`
}

// TextMatch represents a text match from a code search result.
type TextMatch struct {
ObjectURL string `json:"object_url,omitempty"`
ObjectType string `json:"object_type,omitempty"`
Property string `json:"property,omitempty"`
Fragment string `json:"fragment,omitempty"`
Matches []Match `json:"matches,omitempty"`
}

// Match represents an individual match within a text match fragment.
type Match struct {
Text string `json:"text,omitempty"`
Indices []int `json:"indices,omitempty"`
}

// IssueSearchResult wraps the output of search_issues for structured output.
type IssueSearchResult struct {
TotalCount int `json:"total_count"`
IncompleteResults bool `json:"incomplete_results"`
Items []MinimalIssue `json:"items"`
}

// PullRequestSearchResult wraps the output of search_pull_requests for structured output.
// Note: GitHub's search API returns issue-shaped results for PRs, so items use MinimalIssue.
type PullRequestSearchResult struct {
TotalCount int `json:"total_count"`
IncompleteResults bool `json:"incomplete_results"`
Items []MinimalIssue `json:"items"`
}

type MinimalProjectStatusUpdate struct {
ID string `json:"id"`
Body string `json:"body,omitempty"`
Expand Down Expand Up @@ -883,3 +935,45 @@ func convertToMinimalReviewComment(c reviewCommentNode) MinimalReviewComment {

return m
}

func convertToCodeSearchResult(result *github.CodeSearchResult) CodeSearchResult {
items := make([]CodeSearchResultItem, 0, len(result.CodeResults))
for _, cr := range result.CodeResults {
item := CodeSearchResultItem{
Name: cr.GetName(),
Path: cr.GetPath(),
SHA: cr.GetSHA(),
HTMLURL: cr.GetHTMLURL(),
}
if repo := cr.GetRepository(); repo != nil {
item.Repository = &MinimalRepository{
ID: repo.GetID(),
Name: repo.GetName(),
FullName: repo.GetFullName(),
HTMLURL: repo.GetHTMLURL(),
}
}
for _, tm := range cr.TextMatches {
textMatch := TextMatch{
ObjectURL: tm.GetObjectURL(),
ObjectType: tm.GetObjectType(),
Property: tm.GetProperty(),
Fragment: tm.GetFragment(),
}
for _, m := range tm.Matches {
match := Match{
Text: m.GetText(),
Indices: m.Indices,
}
textMatch.Matches = append(textMatch.Matches, match)
}
item.TextMatches = append(item.TextMatches, textMatch)
}
items = append(items, item)
}
return CodeSearchResult{
TotalCount: result.GetTotal(),
IncompleteResults: result.GetIncompleteResults(),
Items: items,
}
}
Loading
Loading