Skip to content
Draft
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
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (any of)**: `repo`, `read:org`
- **Accepted OAuth Scopes**: `admin:org`, `read:org`, `repo`, `write:org`
Comment thread
kelsey-myers marked this conversation as resolved.
- `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`
Expand Down
10 changes: 9 additions & 1 deletion cmd/github-mcp-server/generate_docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion docs/feature-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion docs/insiders-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 3 additions & 4 deletions pkg/errors/error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -687,4 +687,3 @@ func TestNewGitHubAPIErrorResponse_RateLimits(t *testing.T) {
assert.Contains(t, text, "validation failed")
})
}

8 changes: 6 additions & 2 deletions pkg/github/__toolsnaps__/list_issue_types.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
},
Expand Down
49 changes: 45 additions & 4 deletions pkg/github/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
24 changes: 24 additions & 0 deletions pkg/github/issues_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading