Skip to content

Commit 29373fd

Browse files
committed
add list issue types action
1 parent 3deaca8 commit 29373fd

File tree

4 files changed

+214
-0
lines changed

4 files changed

+214
-0
lines changed
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: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"fmt"
77
"io"
8+
"log"
89
"net/http"
910
"strings"
1011
"time"
@@ -19,6 +20,7 @@ import (
1920

2021
// GetIssue creates a tool to get details of a specific issue in a GitHub repository.
2122
func GetIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
23+
log.Println("Fetching issue from GitHub:")
2224
return mcp.NewTool("get_issue",
2325
mcp.WithDescription(t("TOOL_GET_ISSUE_DESCRIPTION", "Get details of a specific issue in a GitHub repository.")),
2426
mcp.WithToolAnnotation(mcp.ToolAnnotation{
@@ -79,6 +81,53 @@ func GetIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool
7981
}
8082
}
8183

84+
// 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.
85+
func ListIssueTypes(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
86+
87+
return mcp.NewTool("list_issue_types",
88+
mcp.WithDescription(t("TOOL_LIST_ISSUE_TYPES_FOR_ORG", "List supported issue types for repository owner (organization).")),
89+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
90+
Title: t("TOOL_LIST_ISSUE_TYPES_USER_TITLE", "List available issue types"),
91+
ReadOnlyHint: ToBoolPtr(true),
92+
}),
93+
mcp.WithString("owner",
94+
mcp.Required(),
95+
mcp.Description("The organization owner of the repository"),
96+
),
97+
),
98+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
99+
owner, err := RequiredParam[string](request, "owner")
100+
if err != nil {
101+
return mcp.NewToolResultError(err.Error()), nil
102+
}
103+
104+
client, err := getClient(ctx)
105+
if err != nil {
106+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
107+
}
108+
issue_types, resp, err := client.Organizations.ListIssueTypes(ctx, owner)
109+
if err != nil {
110+
return nil, fmt.Errorf("failed to list issue types: %w", err)
111+
}
112+
defer func() { _ = resp.Body.Close() }()
113+
114+
if resp.StatusCode != http.StatusOK {
115+
body, err := io.ReadAll(resp.Body)
116+
if err != nil {
117+
return nil, fmt.Errorf("failed to read response body: %w", err)
118+
}
119+
return mcp.NewToolResultError(fmt.Sprintf("failed to list issue types: %s", string(body))), nil
120+
}
121+
122+
r, err := json.Marshal(issue_types)
123+
if err != nil {
124+
return nil, fmt.Errorf("failed to marshal issue types: %w", err)
125+
}
126+
127+
return mcp.NewToolResultText(string(r)), nil
128+
}
129+
}
130+
82131
// AddIssueComment creates a tool to add a comment to an issue.
83132
func AddIssueComment(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
84133
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

@@ -1629,3 +1630,146 @@ func TestAssignCopilotToIssue(t *testing.T) {
16291630
})
16301631
}
16311632
}
1633+
1634+
func Test_ListIssueTypes(t *testing.T) {
1635+
// Verify tool definition once
1636+
mockClient := github.NewClient(nil)
1637+
tool, _ := ListIssueTypes(stubGetClientFn(mockClient), translations.NullTranslationHelper)
1638+
require.NoError(t, toolsnaps.Test(tool.Name, tool))
1639+
1640+
assert.Equal(t, "list_issue_types", tool.Name)
1641+
assert.NotEmpty(t, tool.Description)
1642+
assert.Contains(t, tool.InputSchema.Properties, "owner")
1643+
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner"})
1644+
1645+
// Setup mock issue types for success case
1646+
mockIssueTypes := []*github.IssueType{
1647+
{
1648+
ID: github.Ptr(int64(1)),
1649+
Name: github.Ptr("bug"),
1650+
Description: github.Ptr("Something isn't working"),
1651+
Color: github.Ptr("d73a4a"),
1652+
},
1653+
{
1654+
ID: github.Ptr(int64(2)),
1655+
Name: github.Ptr("feature"),
1656+
Description: github.Ptr("New feature or enhancement"),
1657+
Color: github.Ptr("a2eeef"),
1658+
},
1659+
}
1660+
1661+
tests := []struct {
1662+
name string
1663+
mockedClient *http.Client
1664+
requestArgs map[string]interface{}
1665+
expectError bool
1666+
expectedIssueTypes []*github.IssueType
1667+
expectedErrMsg string
1668+
}{
1669+
{
1670+
name: "successful issue types retrieval",
1671+
mockedClient: mock.NewMockedHTTPClient(
1672+
mock.WithRequestMatchHandler(
1673+
mock.EndpointPattern{
1674+
Pattern: "/orgs/testorg/issue-types",
1675+
Method: "GET",
1676+
},
1677+
mockResponse(t, http.StatusOK, mockIssueTypes),
1678+
),
1679+
),
1680+
requestArgs: map[string]interface{}{
1681+
"owner": "testorg",
1682+
},
1683+
expectError: false,
1684+
expectedIssueTypes: mockIssueTypes,
1685+
},
1686+
{
1687+
name: "organization not found",
1688+
mockedClient: mock.NewMockedHTTPClient(
1689+
mock.WithRequestMatchHandler(
1690+
mock.EndpointPattern{
1691+
Pattern: "/orgs/nonexistent/issue-types",
1692+
Method: "GET",
1693+
},
1694+
mockResponse(t, http.StatusNotFound, `{"message": "Organization not found"}`),
1695+
),
1696+
),
1697+
requestArgs: map[string]interface{}{
1698+
"owner": "nonexistent",
1699+
},
1700+
expectError: true,
1701+
expectedErrMsg: "failed to list issue types",
1702+
},
1703+
{
1704+
name: "missing owner parameter",
1705+
mockedClient: mock.NewMockedHTTPClient(
1706+
mock.WithRequestMatchHandler(
1707+
mock.EndpointPattern{
1708+
Pattern: "/orgs/testorg/issue-types",
1709+
Method: "GET",
1710+
},
1711+
mockResponse(t, http.StatusOK, mockIssueTypes),
1712+
),
1713+
),
1714+
requestArgs: map[string]interface{}{},
1715+
expectError: false, // This should be handled by parameter validation, error returned in result
1716+
expectedErrMsg: "missing required parameter: owner",
1717+
},
1718+
}
1719+
1720+
for _, tc := range tests {
1721+
t.Run(tc.name, func(t *testing.T) {
1722+
// Setup client with mock
1723+
client := github.NewClient(tc.mockedClient)
1724+
_, handler := ListIssueTypes(stubGetClientFn(client), translations.NullTranslationHelper)
1725+
1726+
// Create call request
1727+
request := createMCPRequest(tc.requestArgs)
1728+
1729+
// Call handler
1730+
result, err := handler(context.Background(), request)
1731+
1732+
// Verify results
1733+
if tc.expectError {
1734+
if err != nil {
1735+
assert.Contains(t, err.Error(), tc.expectedErrMsg)
1736+
return
1737+
}
1738+
// Check if error is returned as tool result error
1739+
require.NotNil(t, result)
1740+
require.True(t, result.IsError)
1741+
errorContent := getErrorResult(t, result)
1742+
assert.Contains(t, errorContent.Text, tc.expectedErrMsg)
1743+
return
1744+
}
1745+
1746+
// Check if it's a parameter validation error (returned as tool result error)
1747+
if result != nil && result.IsError {
1748+
errorContent := getErrorResult(t, result)
1749+
if tc.expectedErrMsg != "" && strings.Contains(errorContent.Text, tc.expectedErrMsg) {
1750+
return // This is expected for parameter validation errors
1751+
}
1752+
}
1753+
1754+
require.NoError(t, err)
1755+
require.NotNil(t, result)
1756+
require.False(t, result.IsError)
1757+
textContent := getTextResult(t, result)
1758+
1759+
// Unmarshal and verify the result
1760+
var returnedIssueTypes []*github.IssueType
1761+
err = json.Unmarshal([]byte(textContent.Text), &returnedIssueTypes)
1762+
require.NoError(t, err)
1763+
1764+
if tc.expectedIssueTypes != nil {
1765+
require.Equal(t, len(tc.expectedIssueTypes), len(returnedIssueTypes))
1766+
for i, expected := range tc.expectedIssueTypes {
1767+
assert.Equal(t, *expected.Name, *returnedIssueTypes[i].Name)
1768+
assert.Equal(t, *expected.Description, *returnedIssueTypes[i].Description)
1769+
assert.Equal(t, *expected.Color, *returnedIssueTypes[i].Color)
1770+
assert.Equal(t, *expected.ID, *returnedIssueTypes[i].ID)
1771+
}
1772+
}
1773+
})
1774+
}
1775+
}

pkg/github/tools.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
5353
toolsets.NewServerTool(SearchIssues(getClient, t)),
5454
toolsets.NewServerTool(ListIssues(getClient, t)),
5555
toolsets.NewServerTool(GetIssueComments(getClient, t)),
56+
toolsets.NewServerTool(ListIssueTypes(getClient, t)),
5657
).
5758
AddWriteTools(
5859
toolsets.NewServerTool(CreateIssue(getClient, t)),

0 commit comments

Comments
 (0)