bc5d08da5c
Add NewGitHubAPIStatusErrorResponse helper function to properly track GitHub API errors when the API call succeeds but returns an unexpected HTTP status code (e.g., 404, 422, 500). Previously, these errors were returned via utils.NewToolResultError which bypasses the context-based error tracking used by the remote server's error_categorizer.go for observability metrics. This resulted in 100% tool call success rates in observability even when errors occurred. The fix adds a new helper function that: 1. Creates a synthetic error from the status code and response body 2. Records the error in context via NewGitHubAPIErrorResponse 3. Returns the MCP error result to the client Updated all tool files to use the new pattern for status code errors: - pullrequests.go: 12 fixes - repositories.go: 18 fixes - issues.go: 10 fixes - notifications.go: 6 fixes - projects.go: 5 fixes - search.go: 3 fixes - search_utils.go: 1 fix - gists.go: 4 fixes - code_scanning.go: 2 fixes - dependabot.go: 2 fixes - secret_scanning.go: 2 fixes - security_advisories.go: 4 fixes Total: ~69 error paths now properly tracked. Note: Parameter validation errors (RequiredParam failures) and internal I/O errors (io.ReadAll failures) intentionally continue to use utils.NewToolResultError as they are not GitHub API errors.
118 lines
3.3 KiB
Go
118 lines
3.3 KiB
Go
package github
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"regexp"
|
|
|
|
ghErrors "github.com/github/github-mcp-server/pkg/errors"
|
|
"github.com/github/github-mcp-server/pkg/utils"
|
|
"github.com/google/go-github/v79/github"
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
func hasFilter(query, filterType string) bool {
|
|
// Match filter at start of string, after whitespace, or after non-word characters like '('
|
|
pattern := fmt.Sprintf(`(^|\s|\W)%s:\S+`, regexp.QuoteMeta(filterType))
|
|
matched, _ := regexp.MatchString(pattern, query)
|
|
return matched
|
|
}
|
|
|
|
func hasSpecificFilter(query, filterType, filterValue string) bool {
|
|
// Match specific filter:value at start, after whitespace, or after non-word characters
|
|
// End with word boundary, whitespace, or non-word characters like ')'
|
|
pattern := fmt.Sprintf(`(^|\s|\W)%s:%s($|\s|\W)`, regexp.QuoteMeta(filterType), regexp.QuoteMeta(filterValue))
|
|
matched, _ := regexp.MatchString(pattern, query)
|
|
return matched
|
|
}
|
|
|
|
func hasRepoFilter(query string) bool {
|
|
return hasFilter(query, "repo")
|
|
}
|
|
|
|
func hasTypeFilter(query string) bool {
|
|
return hasFilter(query, "type")
|
|
}
|
|
|
|
func searchHandler(
|
|
ctx context.Context,
|
|
getClient GetClientFn,
|
|
args map[string]any,
|
|
searchType string,
|
|
errorPrefix string,
|
|
) (*mcp.CallToolResult, error) {
|
|
query, err := RequiredParam[string](args, "query")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil
|
|
}
|
|
|
|
if !hasSpecificFilter(query, "is", searchType) {
|
|
query = fmt.Sprintf("is:%s %s", searchType, query)
|
|
}
|
|
|
|
owner, err := OptionalParam[string](args, "owner")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil
|
|
}
|
|
|
|
repo, err := OptionalParam[string](args, "repo")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil
|
|
}
|
|
|
|
if owner != "" && repo != "" && !hasRepoFilter(query) {
|
|
query = fmt.Sprintf("repo:%s/%s %s", owner, repo, query)
|
|
}
|
|
|
|
sort, err := OptionalParam[string](args, "sort")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil
|
|
}
|
|
order, err := OptionalParam[string](args, "order")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil
|
|
}
|
|
pagination, err := OptionalPaginationParams(args)
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil
|
|
}
|
|
|
|
opts := &github.SearchOptions{
|
|
// Default to "created" if no sort is provided, as it's a common use case.
|
|
Sort: sort,
|
|
Order: order,
|
|
ListOptions: github.ListOptions{
|
|
Page: pagination.Page,
|
|
PerPage: pagination.PerPage,
|
|
},
|
|
}
|
|
|
|
client, err := getClient(ctx)
|
|
if err != nil {
|
|
return utils.NewToolResultErrorFromErr(errorPrefix+": failed to get GitHub client", err), nil
|
|
}
|
|
result, resp, err := client.Search.Issues(ctx, query, opts)
|
|
if err != nil {
|
|
return utils.NewToolResultErrorFromErr(errorPrefix, err), nil
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return utils.NewToolResultErrorFromErr(errorPrefix+": failed to read response body", err), nil
|
|
}
|
|
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, errorPrefix, resp, body), nil
|
|
}
|
|
|
|
r, err := json.Marshal(result)
|
|
if err != nil {
|
|
return utils.NewToolResultErrorFromErr(errorPrefix+": failed to marshal response", err), nil
|
|
}
|
|
|
|
return utils.NewToolResultText(string(r)), nil
|
|
}
|