Skip to content
Open
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
5 changes: 5 additions & 0 deletions pkg/github/__toolsnaps__/sub_issue_write.snap
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
},
"method": {
"description": "The action to perform on a single sub-issue\nOptions are:\n- 'add' - add a sub-issue to a parent issue in a GitHub repository.\n- 'remove' - remove a sub-issue from a parent issue in a GitHub repository.\n- 'reprioritize' - change the order of sub-issues within a parent issue in a GitHub repository. Use either 'after_id' or 'before_id' to specify the new position.\n\t\t\t\t",
"enum": [
"add",
"remove",
"reprioritize"
],
"type": "string"
},
"owner": {
Expand Down
39 changes: 15 additions & 24 deletions pkg/github/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ const (
actionsMethodDeleteWorkflowRunLogs = "delete_workflow_run_logs"
)

// Method sets for the consolidated actions tools. Each slice is the single
// source for its tool: it feeds both the schema enum (methodEnum) and the
// unknown-method error (unknownMethodError). Order matches the advertised enum.
var (
actionsWorkflowListMethods = []string{actionsMethodListWorkflows, actionsMethodListWorkflowRuns, actionsMethodListWorkflowJobs, actionsMethodListWorkflowArtifacts}
actionsWorkflowGetMethods = []string{actionsMethodGetWorkflow, actionsMethodGetWorkflowRun, actionsMethodGetWorkflowJob, actionsMethodDownloadWorkflowArtifact, actionsMethodGetWorkflowRunUsage, actionsMethodGetWorkflowRunLogsURL}
actionsWorkflowRunMethods = []string{actionsMethodRunWorkflow, actionsMethodRerunWorkflowRun, actionsMethodRerunFailedJobs, actionsMethodCancelWorkflowRun, actionsMethodDeleteWorkflowRunLogs}
)

// handleFailedJobLogs gets logs for all failed jobs in a workflow run
func handleFailedJobLogs(ctx context.Context, client *github.Client, owner, repo string, runID int64, returnContent bool, tailLines int, contentWindowSize int) (*mcp.CallToolResult, any, error) {
// First, get all jobs for the workflow run
Expand Down Expand Up @@ -217,12 +226,7 @@ Use this tool to list workflows in a repository, or list workflow runs, jobs, an
"method": {
Type: "string",
Description: "The action to perform",
Enum: []any{
actionsMethodListWorkflows,
actionsMethodListWorkflowRuns,
actionsMethodListWorkflowJobs,
actionsMethodListWorkflowArtifacts,
},
Enum: methodEnum(actionsWorkflowListMethods),
},
"owner": {
Type: "string",
Expand Down Expand Up @@ -397,7 +401,7 @@ Use this tool to list workflows in a repository, or list workflow runs, jobs, an
result, payload, err := listWorkflowArtifacts(ctx, client, owner, repo, resourceIDInt, pagination)
return attachIFC(result), payload, err
default:
return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
return unknownMethodError(method, actionsWorkflowListMethods), nil, nil
}
},
)
Expand All @@ -423,14 +427,7 @@ Use this tool to get details about individual workflows, workflow runs, jobs, an
"method": {
Type: "string",
Description: "The method to execute",
Enum: []any{
actionsMethodGetWorkflow,
actionsMethodGetWorkflowRun,
actionsMethodGetWorkflowJob,
actionsMethodDownloadWorkflowArtifact,
actionsMethodGetWorkflowRunUsage,
actionsMethodGetWorkflowRunLogsURL,
},
Enum: methodEnum(actionsWorkflowGetMethods),
},
"owner": {
Type: "string",
Expand Down Expand Up @@ -519,7 +516,7 @@ Use this tool to get details about individual workflows, workflow runs, jobs, an
result, payload, err := getWorkflowRunLogsURL(ctx, client, owner, repo, resourceIDInt)
return attachIFC(result), payload, err
default:
return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
return unknownMethodError(method, actionsWorkflowGetMethods), nil, nil
}
},
)
Expand All @@ -544,13 +541,7 @@ func ActionsRunTrigger(t translations.TranslationHelperFunc) inventory.ServerToo
"method": {
Type: "string",
Description: "The method to execute",
Enum: []any{
actionsMethodRunWorkflow,
actionsMethodRerunWorkflowRun,
actionsMethodRerunFailedJobs,
actionsMethodCancelWorkflowRun,
actionsMethodDeleteWorkflowRunLogs,
},
Enum: methodEnum(actionsWorkflowRunMethods),
},
"owner": {
Type: "string",
Expand Down Expand Up @@ -636,7 +627,7 @@ func ActionsRunTrigger(t translations.TranslationHelperFunc) inventory.ServerToo
case actionsMethodDeleteWorkflowRunLogs:
return deleteWorkflowRunLogs(ctx, client, owner, repo, int64(runID))
default:
return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
return unknownMethodError(method, actionsWorkflowRunMethods), nil, nil
}
},
)
Expand Down
14 changes: 11 additions & 3 deletions pkg/github/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ import (
"github.com/shurcooL/githubv4"
)

// Method sets for the consolidated issue tools (single source for the schema
// enum via methodEnum and the unknown-method error via unknownMethodError).
var (
issueReadMethods = []string{"get", "get_comments", "get_sub_issues", "get_labels"}
subIssueWriteMethods = []string{"add", "remove", "reprioritize"}
)

// CloseIssueInput represents the input for closing an issue via the GraphQL API.
// Used to extend the functionality of the githubv4 library to support closing issues as duplicates.
type CloseIssueInput struct {
Expand Down Expand Up @@ -736,7 +743,7 @@ Options are:
3. get_sub_issues - Get sub-issues of the issue.
4. get_labels - Get labels assigned to the issue.
`,
Enum: []any{"get", "get_comments", "get_sub_issues", "get_labels"},
Enum: methodEnum(issueReadMethods),
},
"owner": {
Type: "string",
Expand Down Expand Up @@ -820,7 +827,7 @@ Options are:
result, err := GetIssueLabels(ctx, gqlClient, owner, repo, issueNumber)
return attachIFC(result), nil, err
default:
return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
return unknownMethodError(method, issueReadMethods), nil, nil
}
})
}
Expand Down Expand Up @@ -1234,6 +1241,7 @@ func SubIssueWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
Properties: map[string]*jsonschema.Schema{
"method": {
Type: "string",
Enum: methodEnum(subIssueWriteMethods),
Description: `The action to perform on a single sub-issue
Options are:
- 'add' - add a sub-issue to a parent issue in a GitHub repository.
Expand Down Expand Up @@ -1327,7 +1335,7 @@ Options are:
result, err := ReprioritizeSubIssue(ctx, client, owner, repo, issueNumber, subIssueID, afterID, beforeID)
return result, nil, err
default:
return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
return unknownMethodError(method, subIssueWriteMethods), nil, nil
}
})
st.FeatureFlagDisable = []string{FeatureFlagIssuesGranular}
Expand Down
23 changes: 23 additions & 0 deletions pkg/github/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import (
"fmt"
"math"
"strconv"
"strings"

"github.com/github/github-mcp-server/pkg/utils"
"github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

// OptionalParamOK is a helper function that can be used to fetch a requested parameter from the request.
Expand Down Expand Up @@ -488,3 +491,23 @@ func (p PaginationParams) ToGraphQLParams() (*GraphQLPaginationParams, error) {
}
return cursor.ToGraphQLParams()
}

// methodEnum renders a tool's supported method list as the []any value the
// input-schema Enum field expects. Declaring the enum from the same []string
// the handler validates against keeps the advertised methods and the runtime
// check from drifting apart.
func methodEnum(methods []string) []any {
out := make([]any, len(methods))
for i, m := range methods {
out[i] = m
}
return out
}

// unknownMethodError is the tool-result error returned when a method-dispatch
// tool is called with a value outside its advertised method enum. Listing the
// supported methods lets the caller (often an LLM) self-correct. supported must
// be the same list advertised in the tool's input-schema enum.
func unknownMethodError(method string, supported []string) *mcp.CallToolResult {
return utils.NewToolResultError(fmt.Sprintf("unknown method: %s. Supported methods are: %s", method, strings.Join(supported, ", ")))
}
16 changes: 16 additions & 0 deletions pkg/github/params_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -642,3 +642,19 @@ func TestOptionalPaginationParams(t *testing.T) {
})
}
}

func TestMethodEnum(t *testing.T) {
// methodEnum must preserve methods and order so the advertised schema enum
// stays byte-identical to the source slice.
assert.Equal(t, []any{"create", "submit_pending"}, methodEnum([]string{"create", "submit_pending"}))
assert.Empty(t, methodEnum(nil))
}

func TestUnknownMethodError(t *testing.T) {
res := unknownMethodError("bogus", []string{"create", "submit_pending", "delete_pending"})
assert.NotNil(t, res)
assert.True(t, res.IsError)
txt := getErrorResult(t, res).Text
assert.Contains(t, txt, "unknown method: bogus")
assert.Contains(t, txt, "Supported methods are: create, submit_pending, delete_pending")
}
38 changes: 15 additions & 23 deletions pkg/github/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ const (
projectsMethodCreateIterationField = "create_iteration_field"
)

// Method sets for the consolidated project tools. Single source per tool: feeds
// both the schema enum (methodEnum) and the unknown-method error
// (unknownMethodError). Order matches the advertised enum.
var (
projectsListMethods = []string{projectsMethodListProjects, projectsMethodListProjectFields, projectsMethodListProjectItems, projectsMethodListProjectStatusUpdates}
projectsGetMethods = []string{projectsMethodGetProject, projectsMethodGetProjectField, projectsMethodGetProjectItem, projectsMethodGetProjectStatusUpdate}
projectsWriteMethods = []string{projectsMethodAddProjectItem, projectsMethodUpdateProjectItem, projectsMethodDeleteProjectItem, projectsMethodCreateProjectStatusUpdate, projectsMethodCreateProject, projectsMethodCreateIterationField}
)

// GraphQL types for ProjectV2 status updates

type statusUpdateNode struct {
Expand Down Expand Up @@ -169,12 +178,7 @@ Use this tool to list projects for a user or organization, or list project field
"method": {
Type: "string",
Description: "The action to perform",
Enum: []any{
projectsMethodListProjects,
projectsMethodListProjectFields,
projectsMethodListProjectItems,
projectsMethodListProjectStatusUpdates,
},
Enum: methodEnum(projectsListMethods),
},
"owner_type": {
Type: "string",
Expand Down Expand Up @@ -284,7 +288,7 @@ Use this tool to list projects for a user or organization, or list project field
result = attachStaticIFCLabel(ctx, deps, result, ifc.LabelProjectContent(isPrivate))
return result, payload, err
default:
return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
return unknownMethodError(method, projectsListMethods), nil, nil
}
default:
return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
Expand Down Expand Up @@ -313,12 +317,7 @@ Use this tool to get details about individual projects, project fields, and proj
"method": {
Type: "string",
Description: "The method to execute",
Enum: []any{
projectsMethodGetProject,
projectsMethodGetProjectField,
projectsMethodGetProjectItem,
projectsMethodGetProjectStatusUpdate,
},
Enum: methodEnum(projectsGetMethods),
},
"owner_type": {
Type: "string",
Expand Down Expand Up @@ -442,7 +441,7 @@ Use this tool to get details about individual projects, project fields, and proj
}
return result, payload, err
default:
return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
return unknownMethodError(method, projectsGetMethods), nil, nil
}
},
)
Expand All @@ -467,14 +466,7 @@ func ProjectsWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
"method": {
Type: "string",
Description: "The method to execute",
Enum: []any{
projectsMethodAddProjectItem,
projectsMethodUpdateProjectItem,
projectsMethodDeleteProjectItem,
projectsMethodCreateProjectStatusUpdate,
projectsMethodCreateProject,
projectsMethodCreateIterationField,
},
Enum: methodEnum(projectsWriteMethods),
},
"owner_type": {
Type: "string",
Expand Down Expand Up @@ -692,7 +684,7 @@ func ProjectsWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
case projectsMethodCreateIterationField:
return createIterationField(ctx, gqlClient, owner, ownerType, projectNumber, args)
default:
return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
return unknownMethodError(method, projectsWriteMethods), nil, nil
}
},
)
Expand Down
4 changes: 3 additions & 1 deletion pkg/github/projects_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func Test_ProjectsList_ListProjects(t *testing.T) {
"owner_type": "org",
},
expectError: true,
expectedErrMsg: "unknown method: unknown_method",
expectedErrMsg: "unknown method: unknown_method. Supported methods are:",
},
}

Expand Down Expand Up @@ -625,6 +625,7 @@ func Test_ProjectsGet_GetProject(t *testing.T) {
require.True(t, result.IsError)
textContent := getTextResult(t, result)
assert.Contains(t, textContent.Text, "unknown method: unknown_method")
assert.Contains(t, textContent.Text, "Supported methods are:")
})
}

Expand Down Expand Up @@ -1113,6 +1114,7 @@ func Test_ProjectsWrite_AddProjectItem(t *testing.T) {
require.True(t, result.IsError)
textContent := getTextResult(t, result)
assert.Contains(t, textContent.Text, "unknown method: unknown_method")
assert.Contains(t, textContent.Text, "Supported methods are:")
})
}

Expand Down
24 changes: 20 additions & 4 deletions pkg/github/pullrequests.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ import (
"github.com/github/github-mcp-server/pkg/utils"
)

// Method sets for the consolidated pull request tools (single source for the
// schema enum via methodEnum and the unknown-method error via unknownMethodError).
var (
pullRequestReadMethods = []string{"get", "get_diff", "get_status", "get_files", "get_commits", "get_review_comments", "get_reviews", "get_comments", "get_check_runs"}
pullRequestReviewWriteMethods = []string{"create", "submit_pending", "delete_pending", "resolve_thread", "unresolve_thread"}
)

// PullRequestRead creates a tool to get details of a specific pull request.
func PullRequestRead(t translations.TranslationHelperFunc) inventory.ServerTool {
schema := &jsonschema.Schema{
Expand All @@ -42,7 +49,7 @@ Possible options:
8. get_comments - Get comments on a pull request. Use this if user doesn't specifically want review comments. Use with pagination parameters to control the number of results returned.
9. get_check_runs - Get check runs for the head commit of a pull request. Check runs are the individual CI/CD jobs and checks that run on the PR.
`,
Enum: []any{"get", "get_diff", "get_status", "get_files", "get_commits", "get_review_comments", "get_reviews", "get_comments", "get_check_runs"},
Enum: methodEnum(pullRequestReadMethods),
},
"owner": {
Type: "string",
Expand Down Expand Up @@ -155,7 +162,7 @@ Possible options:
result, err := GetPullRequestCheckRuns(ctx, client, owner, repo, pullNumber, pagination)
return attachIFC(result), nil, err
default:
return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
return unknownMethodError(method, pullRequestReadMethods), nil, nil
}
})
}
Expand Down Expand Up @@ -1728,7 +1735,7 @@ func PullRequestReviewWrite(t translations.TranslationHelperFunc) inventory.Serv
"method": {
Type: "string",
Description: `The write operation to perform on pull request review.`,
Enum: []any{"create", "submit_pending", "delete_pending", "resolve_thread", "unresolve_thread"},
Enum: methodEnum(pullRequestReviewWriteMethods),
},
"owner": {
Type: "string",
Expand Down Expand Up @@ -1789,6 +1796,15 @@ Available methods:
return utils.NewToolResultError(err.Error()), nil, nil
}

// Unlike the other method-dispatch tools, this handler decodes args
// with WeakDecode rather than RequiredParam, so an omitted method
// would otherwise reach the switch default as an empty value. Enforce
// it explicitly so a missing method is reported the same way as every
// other missing required parameter.
if _, err := RequiredParam[string](args, "method"); err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}

// Given our owner, repo and PR number, lookup the GQL ID of the PR.
client, err := deps.GetGQLClient(ctx)
if err != nil {
Expand All @@ -1812,7 +1828,7 @@ Available methods:
result, err := ResolveReviewThread(ctx, client, params.ThreadID, false)
return result, nil, err
default:
return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", params.Method)), nil, nil
return unknownMethodError(params.Method, pullRequestReviewWriteMethods), nil, nil
}
})
st.FeatureFlagDisable = []string{FeatureFlagPullRequestsGranular}
Expand Down
Loading