Skip to content

Commit 0e7a25e

Browse files
authored
Add List issue types tool (#616)
* add list issue types action * lint * remove log * update docs
1 parent 6f06cba commit 0e7a25e

File tree

5 files changed

+215
-0
lines changed

5 files changed

+215
-0
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,9 @@ The following sets of tools are available (all are on by default):
538538
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
539539
- `repo`: Repository name (string, required)
540540

541+
- **list_issue_types** - List available issue types
542+
- `owner`: The organization owner of the repository (string, required)
543+
541544
- **list_issues** - List issues
542545
- `after`: Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs. (string, optional)
543546
- `direction`: Order direction. If provided, the 'orderBy' also needs to be provided. (string, optional)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"annotations": {
3+
"title": "List available issue types",
4+
"readOnlyHint": true
5+
},
6+
"description": "List supported issue types for repository owner (organization).",
7+
"inputSchema": {
8+
"properties": {
9+
"owner": {
10+
"description": "The organization owner of the repository",
11+
"type": "string"
12+
}
13+
},
14+
"required": [
15+
"owner"
16+
],
17+
"type": "object"
18+
},
19+
"name": "list_issue_types"
20+
}

pkg/github/issues.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,53 @@ func GetIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool
206206
}
207207
}
208208

209+
// 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.
210+
func ListIssueTypes(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
211+
212+
return mcp.NewTool("list_issue_types",
213+
mcp.WithDescription(t("TOOL_LIST_ISSUE_TYPES_FOR_ORG", "List supported issue types for repository owner (organization).")),
214+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
215+
Title: t("TOOL_LIST_ISSUE_TYPES_USER_TITLE", "List available issue types"),
216+
ReadOnlyHint: ToBoolPtr(true),
217+
}),
218+
mcp.WithString("owner",
219+
mcp.Required(),
220+
mcp.Description("The organization owner of the repository"),
221+
),
222+
),
223+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
224+
owner, err := RequiredParam[string](request, "owner")
225+
if err != nil {
226+
return mcp.NewToolResultError(err.Error()), nil
227+
}
228+
229+
client, err := getClient(ctx)
230+
if err != nil {
231+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
232+
}
233+
issueTypes, resp, err := client.Organizations.ListIssueTypes(ctx, owner)
234+
if err != nil {
235+
return nil, fmt.Errorf("failed to list issue types: %w", err)
236+
}
237+
defer func() { _ = resp.Body.Close() }()
238+
239+
if resp.StatusCode != http.StatusOK {
240+
body, err := io.ReadAll(resp.Body)
241+
if err != nil {
242+
return nil, fmt.Errorf("failed to read response body: %w", err)
243+
}
244+
return mcp.NewToolResultError(fmt.Sprintf("failed to list issue types: %s", string(body))), nil
245+
}
246+
247+
r, err := json.Marshal(issueTypes)
248+
if err != nil {
249+
return nil, fmt.Errorf("failed to marshal issue types: %w", err)
250+
}
251+
252+
return mcp.NewToolResultText(string(r)), nil
253+
}
254+
}
255+
209256
// AddIssueComment creates a tool to add a comment to an issue.
210257
func AddIssueComment(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
211258
return mcp.NewTool("add_issue_comment",

pkg/github/issues_test.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"fmt"
77
"net/http"
8+
"strings"
89
"testing"
910
"time"
1011

@@ -2732,3 +2733,146 @@ func Test_ReprioritizeSubIssue(t *testing.T) {
27322733
})
27332734
}
27342735
}
2736+
2737+
func Test_ListIssueTypes(t *testing.T) {
2738+
// Verify tool definition once
2739+
mockClient := github.NewClient(nil)
2740+
tool, _ := ListIssueTypes(stubGetClientFn(mockClient), translations.NullTranslationHelper)
2741+
require.NoError(t, toolsnaps.Test(tool.Name, tool))
2742+
2743+
assert.Equal(t, "list_issue_types", tool.Name)
2744+
assert.NotEmpty(t, tool.Description)
2745+
assert.Contains(t, tool.InputSchema.Properties, "owner")
2746+
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner"})
2747+
2748+
// Setup mock issue types for success case
2749+
mockIssueTypes := []*github.IssueType{
2750+
{
2751+
ID: github.Ptr(int64(1)),
2752+
Name: github.Ptr("bug"),
2753+
Description: github.Ptr("Something isn't working"),
2754+
Color: github.Ptr("d73a4a"),
2755+
},
2756+
{
2757+
ID: github.Ptr(int64(2)),
2758+
Name: github.Ptr("feature"),
2759+
Description: github.Ptr("New feature or enhancement"),
2760+
Color: github.Ptr("a2eeef"),
2761+
},
2762+
}
2763+
2764+
tests := []struct {
2765+
name string
2766+
mockedClient *http.Client
2767+
requestArgs map[string]interface{}
2768+
expectError bool
2769+
expectedIssueTypes []*github.IssueType
2770+
expectedErrMsg string
2771+
}{
2772+
{
2773+
name: "successful issue types retrieval",
2774+
mockedClient: mock.NewMockedHTTPClient(
2775+
mock.WithRequestMatchHandler(
2776+
mock.EndpointPattern{
2777+
Pattern: "/orgs/testorg/issue-types",
2778+
Method: "GET",
2779+
},
2780+
mockResponse(t, http.StatusOK, mockIssueTypes),
2781+
),
2782+
),
2783+
requestArgs: map[string]interface{}{
2784+
"owner": "testorg",
2785+
},
2786+
expectError: false,
2787+
expectedIssueTypes: mockIssueTypes,
2788+
},
2789+
{
2790+
name: "organization not found",
2791+
mockedClient: mock.NewMockedHTTPClient(
2792+
mock.WithRequestMatchHandler(
2793+
mock.EndpointPattern{
2794+
Pattern: "/orgs/nonexistent/issue-types",
2795+
Method: "GET",
2796+
},
2797+
mockResponse(t, http.StatusNotFound, `{"message": "Organization not found"}`),
2798+
),
2799+
),
2800+
requestArgs: map[string]interface{}{
2801+
"owner": "nonexistent",
2802+
},
2803+
expectError: true,
2804+
expectedErrMsg: "failed to list issue types",
2805+
},
2806+
{
2807+
name: "missing owner parameter",
2808+
mockedClient: mock.NewMockedHTTPClient(
2809+
mock.WithRequestMatchHandler(
2810+
mock.EndpointPattern{
2811+
Pattern: "/orgs/testorg/issue-types",
2812+
Method: "GET",
2813+
},
2814+
mockResponse(t, http.StatusOK, mockIssueTypes),
2815+
),
2816+
),
2817+
requestArgs: map[string]interface{}{},
2818+
expectError: false, // This should be handled by parameter validation, error returned in result
2819+
expectedErrMsg: "missing required parameter: owner",
2820+
},
2821+
}
2822+
2823+
for _, tc := range tests {
2824+
t.Run(tc.name, func(t *testing.T) {
2825+
// Setup client with mock
2826+
client := github.NewClient(tc.mockedClient)
2827+
_, handler := ListIssueTypes(stubGetClientFn(client), translations.NullTranslationHelper)
2828+
2829+
// Create call request
2830+
request := createMCPRequest(tc.requestArgs)
2831+
2832+
// Call handler
2833+
result, err := handler(context.Background(), request)
2834+
2835+
// Verify results
2836+
if tc.expectError {
2837+
if err != nil {
2838+
assert.Contains(t, err.Error(), tc.expectedErrMsg)
2839+
return
2840+
}
2841+
// Check if error is returned as tool result error
2842+
require.NotNil(t, result)
2843+
require.True(t, result.IsError)
2844+
errorContent := getErrorResult(t, result)
2845+
assert.Contains(t, errorContent.Text, tc.expectedErrMsg)
2846+
return
2847+
}
2848+
2849+
// Check if it's a parameter validation error (returned as tool result error)
2850+
if result != nil && result.IsError {
2851+
errorContent := getErrorResult(t, result)
2852+
if tc.expectedErrMsg != "" && strings.Contains(errorContent.Text, tc.expectedErrMsg) {
2853+
return // This is expected for parameter validation errors
2854+
}
2855+
}
2856+
2857+
require.NoError(t, err)
2858+
require.NotNil(t, result)
2859+
require.False(t, result.IsError)
2860+
textContent := getTextResult(t, result)
2861+
2862+
// Unmarshal and verify the result
2863+
var returnedIssueTypes []*github.IssueType
2864+
err = json.Unmarshal([]byte(textContent.Text), &returnedIssueTypes)
2865+
require.NoError(t, err)
2866+
2867+
if tc.expectedIssueTypes != nil {
2868+
require.Equal(t, len(tc.expectedIssueTypes), len(returnedIssueTypes))
2869+
for i, expected := range tc.expectedIssueTypes {
2870+
assert.Equal(t, *expected.Name, *returnedIssueTypes[i].Name)
2871+
assert.Equal(t, *expected.Description, *returnedIssueTypes[i].Description)
2872+
assert.Equal(t, *expected.Color, *returnedIssueTypes[i].Color)
2873+
assert.Equal(t, *expected.ID, *returnedIssueTypes[i].ID)
2874+
}
2875+
}
2876+
})
2877+
}
2878+
}

pkg/github/tools.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
5555
toolsets.NewServerTool(SearchIssues(getClient, t)),
5656
toolsets.NewServerTool(ListIssues(getGQLClient, t)),
5757
toolsets.NewServerTool(GetIssueComments(getClient, t)),
58+
toolsets.NewServerTool(ListIssueTypes(getClient, t)),
5859
toolsets.NewServerTool(ListSubIssues(getClient, t)),
5960
).
6061
AddWriteTools(

0 commit comments

Comments
 (0)