Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ jobs:
uses: golangci/golangci-lint-action@v9
with:
# sync with script/lint
version: v2.9
version: v2.14.0
4 changes: 1 addition & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ Thanks for contributing and for helping us build toolsets that are truly valuabl
These are one time installations required to be able to test your changes locally as part of the pull request (PR) submission process.

1. Install Go 1.26.8 or later [through download](https://go.dev/doc/install) | [through Homebrew](https://formulae.brew.sh/formula/go)
2. [Install golangci-lint v2](https://golangci-lint.run/welcome/install/#local-installation)

The repository-pinned golangci-lint v2.9.0 supports Go 1.26, but cannot read Go 1.27 export data. When using a newer Go installation, run lint with `GOTOOLCHAIN=go1.26.8 script/lint`.
2. [Install golangci-lint v2.14.0](https://golangci-lint.run/welcome/install/#local-installation), or let `script/lint` install the repository-pinned version. The pinned version supports both Go 1.26 and Go 1.27.

## Submitting a pull request

Expand Down
2 changes: 1 addition & 1 deletion internal/oauth/testutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func (f *fakeGitHub) handleAuthorize(w http.ResponseWriter, r *http.Request) {
f.mu.Unlock()

redirect := q.Get("redirect_uri") + "?code=authcode&state=" + url.QueryEscape(q.Get("state"))
http.Redirect(w, r, redirect, http.StatusFound)
http.Redirect(w, r, redirect, http.StatusFound) //nolint:gosec // G710: Fake OAuth server redirects to the test client's callback, not an external user's input.
}

func (f *fakeGitHub) handleToken(w http.ResponseWriter, r *http.Request) {
Expand Down
4 changes: 2 additions & 2 deletions pkg/buffer/buffer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,14 @@ func TestProcessResponseAsRingBufferToEnd(t *testing.T) {
// Ring buffer size is 5, so we should only keep the last 5 lines
var sb strings.Builder
for i := 1; i <= 10; i++ {
sb.WriteString(fmt.Sprintf("line%d\n", i))
fmt.Fprintf(&sb, "line%d\n", i)
}
// Insert an 11MB line (exceeds maxLineSize of 10MB)
longLine := strings.Repeat("x", 11*1024*1024)
sb.WriteString(longLine)
sb.WriteString("\n")
for i := 11; i <= 20; i++ {
sb.WriteString(fmt.Sprintf("line%d\n", i))
fmt.Fprintf(&sb, "line%d\n", i)
}

resp := &http.Response{
Expand Down
6 changes: 2 additions & 4 deletions pkg/errors/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,7 @@ func NewGitHubAPIErrorResponse(ctx context.Context, message string, resp *github
_, _ = addGitHubAPIErrorToContext(ctx, apiErr) // Explicitly ignore error for graceful handling
}

var rateLimitErr *github.RateLimitError
if stderrors.As(err, &rateLimitErr) {
if rateLimitErr, ok := stderrors.AsType[*github.RateLimitError](err); ok {
resetTime := rateLimitErr.Rate.Reset.Time
if !resetTime.IsZero() {
retryIn := time.Until(resetTime).Round(time.Second)
Expand All @@ -179,8 +178,7 @@ func NewGitHubAPIErrorResponse(ctx context.Context, message string, resp *github
"%s: GitHub API rate limit exceeded. Wait before retrying.", message))
}

var abuseErr *github.AbuseRateLimitError
if stderrors.As(err, &abuseErr) {
if abuseErr, ok := stderrors.AsType[*github.AbuseRateLimitError](err); ok {
if abuseErr.RetryAfter != nil {
retryAfter := abuseErr.RetryAfter.Round(time.Second)
if retryAfter > 0 {
Expand Down
3 changes: 1 addition & 2 deletions pkg/github/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -1103,8 +1103,7 @@ func rerunFailedJobs(ctx context.Context, client *github.Client, owner, repo str
func cancelWorkflowRun(ctx context.Context, client *github.Client, owner, repo string, runID int64) (*mcp.CallToolResult, any, error) {
resp, err := client.Actions.CancelWorkflowRunByID(ctx, owner, repo, runID)
if err != nil {
var acceptedErr *github.AcceptedError
if !errors.As(err, &acceptedErr) {
if _, ok := errors.AsType[*github.AcceptedError](err); !ok {
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to cancel workflow run", resp, err), nil, nil
}
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/github/copilot.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,15 @@ func (d *mvpDescription) String() string {
sb.WriteString("\n\n")
sb.WriteString("This tool can help with the following outcomes:\n")
for _, outcome := range d.outcomes {
sb.WriteString(fmt.Sprintf("- %s\n", outcome))
fmt.Fprintf(&sb, "- %s\n", outcome)
}
}

if len(d.referenceLinks) > 0 {
sb.WriteString("\n\n")
sb.WriteString("More information can be found at:\n")
for _, link := range d.referenceLinks {
sb.WriteString(fmt.Sprintf("- %s\n", link))
fmt.Fprintf(&sb, "- %s\n", link)
}
}

Expand Down
21 changes: 7 additions & 14 deletions pkg/github/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -625,8 +625,7 @@ Use this tool to get details about individual projects, project fields, project
}
resolvedIDs, resolveErr := resolveFieldNamesToIDs(ctx, gqlClient, owner, ownerType, projectNumber, fieldNames, "fields")
if resolveErr != nil {
var structured *ghErrors.StructuredResolutionError
if errors.As(resolveErr, &structured) {
if structured, ok := errors.AsType[*ghErrors.StructuredResolutionError](resolveErr); ok {
return ghErrors.NewStructuredResolutionErrorResponse(structured), nil, nil
}
return utils.NewToolResultError(resolveErr.Error()), nil, nil
Expand Down Expand Up @@ -984,8 +983,7 @@ func ProjectsWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
// Resolve the item by (item_owner, item_repo, issue_number).
resolvedItemID, resolveErr := resolveItemIDFromIssueArgs(ctx, gqlClient, owner, ownerType, projectNumber, args)
if resolveErr != nil {
var structured *ghErrors.StructuredResolutionError
if errors.As(resolveErr, &structured) {
if structured, ok := errors.AsType[*ghErrors.StructuredResolutionError](resolveErr); ok {
return ghErrors.NewStructuredResolutionErrorResponse(structured), nil, nil
}
return utils.NewToolResultError(resolveErr.Error()), nil, nil
Expand Down Expand Up @@ -1252,8 +1250,7 @@ func listProjectItems(ctx context.Context, client *github.Client, gqlClient *git
if len(fieldNames) > 0 {
resolvedIDs, resolveErr := resolveFieldNamesToIDs(ctx, gqlClient, owner, ownerType, projectNumber, fieldNames, "fields")
if resolveErr != nil {
var structured *ghErrors.StructuredResolutionError
if errors.As(resolveErr, &structured) {
if structured, ok := errors.AsType[*ghErrors.StructuredResolutionError](resolveErr); ok {
return ghErrors.NewStructuredResolutionErrorResponse(structured), nil, nil
}
return utils.NewToolResultError(resolveErr.Error()), nil, nil
Expand Down Expand Up @@ -1446,8 +1443,7 @@ func fetchProjectItem(ctx context.Context, client *github.Client, owner, ownerTy
func updateProjectItem(ctx context.Context, client *github.Client, gqlClient *githubv4.Client, owner, ownerType string, projectNumber int, itemID int64, fieldValue map[string]any) (*mcp.CallToolResult, any, error) {
updatePayload, issueField, err := buildUpdateProjectItem(ctx, gqlClient, owner, ownerType, projectNumber, fieldValue)
if err != nil {
var structured *ghErrors.StructuredResolutionError
if errors.As(err, &structured) {
if structured, ok := errors.AsType[*ghErrors.StructuredResolutionError](err); ok {
return ghErrors.NewStructuredResolutionErrorResponse(structured), nil, nil
}
return utils.NewToolResultError(err.Error()), nil, nil
Expand All @@ -1469,8 +1465,7 @@ func updateProjectItem(ctx context.Context, client *github.Client, gqlClient *gi

issueID, resolveErr := projectItemIssueID(projectItem)
if resolveErr != nil {
var structured *ghErrors.StructuredResolutionError
if errors.As(resolveErr, &structured) {
if structured, ok := errors.AsType[*ghErrors.StructuredResolutionError](resolveErr); ok {
return ghErrors.NewStructuredResolutionErrorResponse(structured), nil, nil
}
return utils.NewToolResultError(resolveErr.Error()), nil, nil
Expand Down Expand Up @@ -2136,8 +2131,7 @@ func createProjectView(ctx context.Context, gqlClient *githubv4.Client, args map
}
configuration, err := projectViewVisibleFieldsInput(ctx, gqlClient, args, owner, ownerType, projectNumber)
if err != nil {
var structured *ghErrors.StructuredResolutionError
if errors.As(err, &structured) {
if structured, ok := errors.AsType[*ghErrors.StructuredResolutionError](err); ok {
return ghErrors.NewStructuredResolutionErrorResponse(structured), nil, nil
}
return utils.NewToolResultError(err.Error()), nil, nil
Expand Down Expand Up @@ -2267,8 +2261,7 @@ func updateProjectView(ctx context.Context, gqlClient *githubv4.Client, args map

configuration, err := projectViewVisibleFieldsInput(ctx, gqlClient, args, owner, ownerType, projectNumber)
if err != nil {
var structured *ghErrors.StructuredResolutionError
if errors.As(err, &structured) {
if structured, ok := errors.AsType[*ghErrors.StructuredResolutionError](err); ok {
return ghErrors.NewStructuredResolutionErrorResponse(structured), nil, nil
}
return utils.NewToolResultError(err.Error()), nil, nil
Expand Down
6 changes: 2 additions & 4 deletions pkg/github/projects_batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,7 @@ func updateProjectItemsBatch(ctx context.Context, client *github.Client, gqlClie
}

func batchTopLevelError(err error) *mcp.CallToolResult {
var structured *ghErrors.StructuredResolutionError
if errors.As(err, &structured) {
if structured, ok := errors.AsType[*ghErrors.StructuredResolutionError](err); ok {
return ghErrors.NewStructuredResolutionErrorResponse(structured)
}
return utils.NewToolResultError(err.Error())
Expand Down Expand Up @@ -796,8 +795,7 @@ func resolveIssueRefs(ctx context.Context, gqlClient *githubv4.Client, projectID
}

func batchErrorFromResolution(err error) *batchItemError {
var structured *ghErrors.StructuredResolutionError
if errors.As(err, &structured) {
if structured, ok := errors.AsType[*ghErrors.StructuredResolutionError](err); ok {
return &batchItemError{
Code: structured.Kind,
Message: fmt.Sprintf("%s: %s", structured.Kind, structured.Name),
Expand Down
2 changes: 1 addition & 1 deletion pkg/github/projects_batch_mutation.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func executeAliasedMutation(ctx context.Context, gqlClient *githubv4.Client, kin
outcomes := make([]mutationAliasOutcome, len(inputs))
elem := mutationPtr.Elem()
for i := range inputs {
result, ok := elem.Field(i).Interface().(projectV2ItemMutationResult)
result, ok := reflect.TypeAssert[projectV2ItemMutationResult](elem.Field(i))
if !ok || result.ProjectV2Item.ID == "" {
continue
}
Expand Down
12 changes: 6 additions & 6 deletions pkg/github/projects_batch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -396,10 +396,10 @@ func Test_ProjectsWrite_UpdateProjectItems_NumericItemIDDeduplicatesRESTLookup(t
}
gqlClient := newTestGQLClient(transport)

var restCalls int32
var restCalls atomic.Int32
restClient := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
GetOrgsProjectsV2ItemsByProjectByItemID: func(w http.ResponseWriter, _ *http.Request) {
atomic.AddInt32(&restCalls, 1)
restCalls.Add(1)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"id":1001,"node_id":"PVTI_item1001"}`))
Expand Down Expand Up @@ -427,7 +427,7 @@ func Test_ProjectsWrite_UpdateProjectItems_NumericItemIDDeduplicatesRESTLookup(t
require.NoError(t, json.Unmarshal([]byte(getTextResult(t, result).Text), &response))
assert.Equal(t, float64(1), response["succeeded"])
assert.Equal(t, float64(1), response["failed"])
assert.Equal(t, int32(1), atomic.LoadInt32(&restCalls), "the same numeric item_id must only be resolved once")
assert.Equal(t, int32(1), restCalls.Load(), "the same numeric item_id must only be resolved once")
results := response["results"].([]any)
assert.Equal(t, "duplicate_target", results[1].(map[string]any)["error"].(map[string]any)["code"])
}
Expand Down Expand Up @@ -581,10 +581,10 @@ func Test_ProjectsWrite_UpdateProjectItems_DuplicateTargetRejected(t *testing.T)
},
}
gqlClient := newTestGQLClient(transport)
var restCalls int32
var restCalls atomic.Int32
restClient := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
GetOrgsProjectsV2ItemsByProjectByItemID: func(w http.ResponseWriter, _ *http.Request) {
atomic.AddInt32(&restCalls, 1)
restCalls.Add(1)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":1001,"node_id":"PVTI_item1"}`))
},
Expand Down Expand Up @@ -616,7 +616,7 @@ func Test_ProjectsWrite_UpdateProjectItems_DuplicateTargetRejected(t *testing.T)
second := results[1].(map[string]any)
assert.Equal(t, "failed", second["status"])
assert.Equal(t, "duplicate_target", second["error"].(map[string]any)["code"])
assert.Equal(t, int32(1), atomic.LoadInt32(&restCalls))
assert.Equal(t, int32(1), restCalls.Load())
assert.Len(t, transport.mutationCalls, 1)
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/github/projects_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ func resolveFieldNamesToIDsFromFields(all []ResolvedField, names []string, owner
for i, field := range resolved {
id, parseErr := parseInt64(field.ID)
if parseErr != nil {
return nil, fmt.Errorf("resolved field %q has non-numeric ID %q; pass it via '%s' instead", names[i], field.ID, idParameter)
return nil, fmt.Errorf("resolved field %q has non-numeric ID %q; pass it via '%s' instead", names[i], field.ID, idParameter) //nolint:gosec // G602: resolveFieldsByName returns exactly one field per name in order, or an error.
}
out = append(out, id)
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/github/secret_scanning.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func GetSecretScanningAlert(t translations.TranslationHelperFunc) inventory.Serv
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get alert", resp, body), nil, nil
}

r, err := json.Marshal(alert)
r, err := json.Marshal(alert) //nolint:gosec // G117: This security_events-scoped tool intentionally returns the alert's secret; the result is labeled private-untrusted.
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal alert: %w", err)
}
Expand Down Expand Up @@ -200,7 +200,7 @@ func ListSecretScanningAlerts(t translations.TranslationHelperFunc) inventory.Se
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to list alerts", resp, body), nil, nil
}

r, err := json.Marshal(alerts)
r, err := json.Marshal(alerts) //nolint:gosec // G117: This security_events-scoped tool intentionally returns alert secrets; the result is labeled private-untrusted.
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal alerts: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/http/middleware/pat_scope_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func TestWithPATScopes(t *testing.T) {
},
{
name: "fine-grained PAT skips scope fetching",
tokenInfo: &ghcontext.TokenInfo{
tokenInfo: &ghcontext.TokenInfo{ //nolint:gosec // G101: Synthetic fine-grained PAT fixture verifies scope fetching is skipped; it cannot authenticate.
Token: "github_pat_xxxxxxxxxxxxxxxxxxxxxxx",
TokenType: utils.TokenTypeFineGrainedPersonalAccessToken,
},
Expand Down
2 changes: 1 addition & 1 deletion pkg/http/middleware/scope_challenge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ func scopeChallengeContext(ctx context.Context) context.Context {
}

func scopeChallengeContextWithScopes(ctx context.Context, activeScopes []string) context.Context {
ctx = ghcontext.WithTokenInfo(ctx, &ghcontext.TokenInfo{
ctx = ghcontext.WithTokenInfo(ctx, &ghcontext.TokenInfo{ //nolint:gosec // G101: "oauth-token" is a synthetic context fixture, not an authentication credential.
Token: "oauth-token",
TokenType: utils.TokenTypeOAuthAccessToken,
})
Expand Down
10 changes: 5 additions & 5 deletions pkg/http/middleware/token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ func TestExtractUserToken(t *testing.T) {
expectWWWAuth: true,
},
// Personal Access Token (classic) - ghp_ prefix
{
{ //nolint:gosec // G101: Synthetic token fixture exercises classic PAT prefix detection; it cannot authenticate.
name: "personal access token (classic) with Bearer prefix",
authHeader: "Bearer ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
expectedStatusCode: http.StatusOK,
expectedTokenType: utils.TokenTypePersonalAccessToken,
expectedToken: "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
expectTokenInfo: true,
},
{
{ //nolint:gosec // G101: Synthetic token fixture exercises lowercase bearer parsing; it cannot authenticate.
name: "personal access token (classic) with bearer lowercase",
authHeader: "bearer ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
expectedStatusCode: http.StatusOK,
Expand All @@ -62,15 +62,15 @@ func TestExtractUserToken(t *testing.T) {
expectTokenInfo: true,
},
// Fine-grained Personal Access Token - github_pat_ prefix
{
{ //nolint:gosec // G101: Synthetic token fixture exercises fine-grained PAT prefix detection; it cannot authenticate.
name: "fine-grained personal access token with Bearer prefix",
authHeader: "Bearer github_pat_xxxxxxxxxxxxxxxxxxxxxxx",
expectedStatusCode: http.StatusOK,
expectedTokenType: utils.TokenTypeFineGrainedPersonalAccessToken,
expectedToken: "github_pat_xxxxxxxxxxxxxxxxxxxxxxx",
expectTokenInfo: true,
},
{
{ //nolint:gosec // G101: Synthetic token fixture exercises fine-grained PAT parsing without a bearer prefix; it cannot authenticate.
name: "fine-grained personal access token without Bearer prefix",
authHeader: "github_pat_xxxxxxxxxxxxxxxxxxxxxxx",
expectedStatusCode: http.StatusOK,
Expand Down Expand Up @@ -113,7 +113,7 @@ func TestExtractUserToken(t *testing.T) {
expectTokenInfo: true,
},
// Server-to-Server GitHub App Token (installation token) - ghs_ prefix
{
{ //nolint:gosec // G101: Synthetic token fixture exercises installation token prefix detection; it cannot authenticate.
name: "server-to-server GitHub App token with Bearer prefix",
authHeader: "Bearer ghs_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
expectedStatusCode: http.StatusOK,
Expand Down
12 changes: 6 additions & 6 deletions pkg/http/transport/etag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ func TestETagTransport_ServesCachedBodyOn304(t *testing.T) {
const etag = `"abc123"`
const body = `{"number":1}`

var requests int32
var requests atomic.Int32
var lastIfNoneMatch string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
n := atomic.AddInt32(&requests, 1)
n := requests.Add(1)
lastIfNoneMatch = r.Header.Get(headers.IfNoneMatchHeader)
w.Header().Set(headers.ETagHeader, etag)
if n == 1 {
Expand Down Expand Up @@ -84,7 +84,7 @@ func TestETagTransport_ServesCachedBodyOn304(t *testing.T) {
assert.Equal(t, http.StatusOK, status2, "304 is translated to the cached 200")
assert.Equal(t, body, body2, "cached body is served on 304")
assert.Equal(t, etag, lastIfNoneMatch, "second request sends the cached ETag")
assert.Equal(t, int32(2), atomic.LoadInt32(&requests), "every request still reaches the server")
assert.Equal(t, int32(2), requests.Load(), "every request still reaches the server")
}

// TestETagTransport_UpdatesRateLimitHeadersFrom304 verifies that a cache-served
Expand All @@ -95,9 +95,9 @@ func TestETagTransport_UpdatesRateLimitHeadersFrom304(t *testing.T) {

const etag = `"v1"`

var requests int32
var requests atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
n := atomic.AddInt32(&requests, 1)
n := requests.Add(1)
w.Header().Set(headers.ETagHeader, etag)
if n == 1 {
w.Header().Set("X-RateLimit-Remaining", "100")
Expand Down Expand Up @@ -143,7 +143,7 @@ func TestETagTransport_ScopesCacheByAuthorization(t *testing.T) {
return
}
w.WriteHeader(http.StatusOK)
_, _ = io.WriteString(w, r.Header.Get(headers.AuthorizationHeader))
_, _ = io.WriteString(w, r.Header.Get(headers.AuthorizationHeader)) //nolint:gosec // G705: Test server echoes synthetic tokens to verify cache isolation; no browser consumes the response.
}))
defer server.Close()

Expand Down
2 changes: 1 addition & 1 deletion pkg/http/transport/user_agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
func TestUserAgentTransportRequestIsolation(t *testing.T) {
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
_, _ = io.WriteString(w, req.UserAgent())
_, _ = io.WriteString(w, req.UserAgent()) //nolint:gosec // G705: Test server echoes the test client's user agent to verify transport isolation; no browser consumes the response.
}))
t.Cleanup(server.Close)
transport := &UserAgentTransport{
Expand Down
5 changes: 3 additions & 2 deletions pkg/inventory/server_tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"maps"
"slices"

"github.com/github/github-mcp-server/pkg/octicons"
"github.com/google/jsonschema-go/jsonschema"
Expand Down Expand Up @@ -142,8 +143,8 @@ func (st *ServerTool) Handler(deps any) mcp.ToolHandler {
// Panics if the tool has no handler - all tools should have handlers.
func (st *ServerTool) RegisterFunc(s *mcp.Server, deps any, middleware ...ToolHandlerMiddleware) {
handler := st.Handler(deps) // This will panic if HandlerFunc is nil
for i := len(middleware) - 1; i >= 0; i-- {
handler = middleware[i](handler)
for _, m := range slices.Backward(middleware) {
handler = m(handler)
}
handler = st.wrapAvailabilityCheck(handler)
// Make a shallow copy of the tool to avoid mutating the original
Expand Down
Loading
Loading