From b677d373b80c111cde4439171a84374a7d89bef6 Mon Sep 17 00:00:00 2001 From: Kelsey Myers <52179263+kelsey-myers@users.noreply.github.com> Date: Mon, 15 Jun 2026 03:42:08 -0700 Subject: [PATCH 1/2] Add repo-scoped support to list_issue_types tool --- README.md | 7 +-- pkg/errors/error_test.go | 7 ++- .../__toolsnaps__/list_issue_types.snap | 8 ++- pkg/github/issues.go | 49 +++++++++++++++++-- pkg/github/issues_test.go | 24 +++++++++ 5 files changed, 82 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index dc063f22ce..857fa20bc6 100644 --- a/README.md +++ b/README.md @@ -875,9 +875,10 @@ The following sets of tools are available: - `type`: Type of this issue. Only use if the repository has issue types configured. Use list_issue_types tool to get valid type values for the organization. If the repository doesn't support issue types, omit this parameter. (string, optional) - **list_issue_types** - List available issue types - - **Required OAuth Scopes**: `read:org` - - **Accepted OAuth Scopes**: `admin:org`, `read:org`, `write:org` - - `owner`: The organization owner of the repository (string, required) + - **Required OAuth Scopes**: `repo`, `read:org` + - **Accepted OAuth Scopes**: `admin:org`, `read:org`, `repo`, `write:org` + - `owner`: The account owner of the repository or organization. (string, required) + - `repo`: The name of the repository. When provided, returns issue types for this specific repository. When omitted, returns org-level issue types directly. (string, optional) - **list_issues** - List issues - **Required OAuth Scopes**: `repo` diff --git a/pkg/errors/error_test.go b/pkg/errors/error_test.go index 77ceb21375..3c899b6b58 100644 --- a/pkg/errors/error_test.go +++ b/pkg/errors/error_test.go @@ -3,13 +3,13 @@ package errors import ( "context" "fmt" - "net/http" - "testing" - "time" "github.com/google/go-github/v87/github" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "net/http" + "testing" + "time" ) func TestGitHubErrorContext(t *testing.T) { @@ -687,4 +687,3 @@ func TestNewGitHubAPIErrorResponse_RateLimits(t *testing.T) { assert.Contains(t, text, "validation failed") }) } - diff --git a/pkg/github/__toolsnaps__/list_issue_types.snap b/pkg/github/__toolsnaps__/list_issue_types.snap index f1f1377a81..283cb5a8de 100644 --- a/pkg/github/__toolsnaps__/list_issue_types.snap +++ b/pkg/github/__toolsnaps__/list_issue_types.snap @@ -3,11 +3,15 @@ "readOnlyHint": true, "title": "List available issue types" }, - "description": "List supported issue types for repository owner (organization).", + "description": "List supported issue types for a repository or its owner organization. When repo is omitted, returns org-level issue types directly.", "inputSchema": { "properties": { "owner": { - "description": "The organization owner of the repository", + "description": "The account owner of the repository or organization.", + "type": "string" + }, + "repo": { + "description": "The name of the repository. When provided, returns issue types for this specific repository. When omitted, returns org-level issue types directly.", "type": "string" } }, diff --git a/pkg/github/issues.go b/pkg/github/issues.go index 27fc0a4abe..b0f6617e6e 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -1067,13 +1067,14 @@ func GetIssueLabels(ctx context.Context, client *githubv4.Client, owner string, return utils.NewToolResultText(string(out)), nil } -// ListIssueTypes creates a tool to list defined issue types for an organization. This can be used to understand supported issue type values for creating or updating issues. +// ListIssueTypes creates a tool to list defined issue types for an organization or repository. +// This can be used to understand supported issue type values for creating or updating issues. func ListIssueTypes(t translations.TranslationHelperFunc) inventory.ServerTool { return NewTool( ToolsetMetadataIssues, mcp.Tool{ Name: "list_issue_types", - Description: t("TOOL_LIST_ISSUE_TYPES_FOR_ORG", "List supported issue types for repository owner (organization)."), + Description: t("TOOL_LIST_ISSUE_TYPES_FOR_ORG", "List supported issue types for a repository or its owner organization. When repo is omitted, returns org-level issue types directly."), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_ISSUE_TYPES_USER_TITLE", "List available issue types"), ReadOnlyHint: true, @@ -1083,23 +1084,63 @@ func ListIssueTypes(t translations.TranslationHelperFunc) inventory.ServerTool { Properties: map[string]*jsonschema.Schema{ "owner": { Type: "string", - Description: "The organization owner of the repository", + Description: "The account owner of the repository or organization.", + }, + "repo": { + Type: "string", + Description: "The name of the repository. When provided, returns issue types for this specific repository. When omitted, returns org-level issue types directly.", }, }, Required: []string{"owner"}, }, }, - []scopes.Scope{scopes.ReadOrg}, + []scopes.Scope{scopes.Repo, scopes.ReadOrg}, func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { owner, err := RequiredParam[string](args, "owner") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } + repo, err := OptionalParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } client, err := deps.GetClient(ctx) if err != nil { return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil } + + if repo != "" { + apiURL := fmt.Sprintf("repos/%s/%s/issue-types", owner, repo) + req, err := client.NewRequest(ctx, "GET", apiURL, nil) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to create request", err), nil, nil + } + var issueTypes []*github.IssueType + resp, err := client.Do(req, &issueTypes) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list issue types", resp, err), nil, nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil + } + return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to list issue types", resp, body), nil, nil + } + + r, err := json.Marshal(issueTypes) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to marshal issue types", err), nil, nil + } + + result := utils.NewToolResultText(string(r)) + result = attachRepoVisibilityIFCLabelLazy(ctx, deps, owner, repo, result, ifc.LabelRepoMetadata) + return result, nil, nil + } + issueTypes, resp, err := client.Organizations.ListIssueTypes(ctx, owner) if err != nil { return utils.NewToolResultErrorFromErr("failed to list issue types", err), nil, nil diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index 7e47cdb527..324d741c20 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -4802,6 +4802,30 @@ func Test_ListIssueTypes(t *testing.T) { expectError: false, // This should be handled by parameter validation, error returned in result expectedErrMsg: "missing required parameter: owner", }, + { + name: "successful repo issue types retrieval", + mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ + "GET /repos/testorg/testrepo/issue-types": mockResponse(t, http.StatusOK, mockIssueTypes), + }), + requestArgs: map[string]any{ + "owner": "testorg", + "repo": "testrepo", + }, + expectError: false, + expectedIssueTypes: mockIssueTypes, + }, + { + name: "repo not found", + mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ + "GET /repos/testorg/nonexistent/issue-types": mockResponse(t, http.StatusNotFound, `{"message": "Not Found"}`), + }), + requestArgs: map[string]any{ + "owner": "testorg", + "repo": "nonexistent", + }, + expectError: true, + expectedErrMsg: "failed to list issue types", + }, } for _, tc := range tests { From 096cdb51282573561ef9e51679ab5c1b33890c16 Mon Sep 17 00:00:00 2001 From: Kelsey Myers <52179263+kelsey-myers@users.noreply.github.com> Date: Mon, 15 Jun 2026 07:59:51 -0700 Subject: [PATCH 2/2] Render multi-scope tools as "any of" in generated docs --- README.md | 2 +- cmd/github-mcp-server/generate_docs.go | 10 +++++++++- docs/feature-flags.md | 2 +- docs/insiders-features.md | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 857fa20bc6..c5e97ae77a 100644 --- a/README.md +++ b/README.md @@ -875,7 +875,7 @@ The following sets of tools are available: - `type`: Type of this issue. Only use if the repository has issue types configured. Use list_issue_types tool to get valid type values for the organization. If the repository doesn't support issue types, omit this parameter. (string, optional) - **list_issue_types** - List available issue types - - **Required OAuth Scopes**: `repo`, `read:org` + - **Required OAuth Scopes (any of)**: `repo`, `read:org` - **Accepted OAuth Scopes**: `admin:org`, `read:org`, `repo`, `write:org` - `owner`: The account owner of the repository or organization. (string, required) - `repo`: The name of the repository. When provided, returns issue types for this specific repository. When omitted, returns org-level issue types directly. (string, optional) diff --git a/cmd/github-mcp-server/generate_docs.go b/cmd/github-mcp-server/generate_docs.go index 78ed8361a8..212851c50d 100644 --- a/cmd/github-mcp-server/generate_docs.go +++ b/cmd/github-mcp-server/generate_docs.go @@ -221,7 +221,15 @@ func writeToolDoc(buf *strings.Builder, tool inventory.ServerTool) { // OAuth scopes if present if len(tool.RequiredScopes) > 0 { - fmt.Fprintf(buf, " - **Required OAuth Scopes**: `%s`\n", strings.Join(tool.RequiredScopes, "`, `")) + // Scope filtering uses "any of" semantics (see scopes.HasRequiredScopes), + // so when multiple required scopes are listed, render them as alternatives + // rather than implying all are required. + scopeList := "`" + strings.Join(tool.RequiredScopes, "`, `") + "`" + if len(tool.RequiredScopes) > 1 { + fmt.Fprintf(buf, " - **Required OAuth Scopes (any of)**: %s\n", scopeList) + } else { + fmt.Fprintf(buf, " - **Required OAuth Scopes**: %s\n", scopeList) + } // Only show accepted scopes if they differ from required scopes if len(tool.AcceptedScopes) > 0 && !scopesEqual(tool.RequiredScopes, tool.AcceptedScopes) { diff --git a/docs/feature-flags.md b/docs/feature-flags.md index cb02463a10..6906b8d918 100644 --- a/docs/feature-flags.md +++ b/docs/feature-flags.md @@ -95,7 +95,7 @@ runtime behavior (such as output formatting) won't appear here. - `type`: Type of this issue. Only use if the repository has issue types configured. Use list_issue_types tool to get valid type values for the organization. If the repository doesn't support issue types, omit this parameter. (string, optional) - **list_issue_fields** - List issue fields - - **Required OAuth Scopes**: `repo`, `read:org` + - **Required OAuth Scopes (any of)**: `repo`, `read:org` - **Accepted OAuth Scopes**: `admin:org`, `read:org`, `repo`, `write:org` - `owner`: The account owner of the repository or organization. The name is not case sensitive. (string, required) - `repo`: The name of the repository. When provided, returns fields for this specific repository (inherited from its organization). When omitted, returns org-level fields directly. (string, optional) diff --git a/docs/insiders-features.md b/docs/insiders-features.md index 2277f0c8e2..677e52208c 100644 --- a/docs/insiders-features.md +++ b/docs/insiders-features.md @@ -89,7 +89,7 @@ The list below is generated from the Go source. It covers tool **inventory and s - `type`: Type of this issue. Only use if the repository has issue types configured. Use list_issue_types tool to get valid type values for the organization. If the repository doesn't support issue types, omit this parameter. (string, optional) - **list_issue_fields** - List issue fields - - **Required OAuth Scopes**: `repo`, `read:org` + - **Required OAuth Scopes (any of)**: `repo`, `read:org` - **Accepted OAuth Scopes**: `admin:org`, `read:org`, `repo`, `write:org` - `owner`: The account owner of the repository or organization. The name is not case sensitive. (string, required) - `repo`: The name of the repository. When provided, returns fields for this specific repository (inherited from its organization). When omitted, returns org-level fields directly. (string, optional)