Skip to content

Commit d8732b5

Browse files
committed
initial OSS granular PRs and issues toolsets
1 parent 95726ad commit d8732b5

File tree

4 files changed

+1748
-0
lines changed

4 files changed

+1748
-0
lines changed

pkg/github/granular_tools_test.go

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"testing"
7+
8+
"github.com/github/github-mcp-server/pkg/inventory"
9+
"github.com/github/github-mcp-server/pkg/translations"
10+
gogithub "github.com/google/go-github/v82/github"
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func granularToolsForToolset(id inventory.ToolsetID) []inventory.ServerTool {
16+
var result []inventory.ServerTool
17+
for _, tool := range AllTools(translations.NullTranslationHelper) {
18+
if tool.Toolset.ID == id {
19+
result = append(result, tool)
20+
}
21+
}
22+
return result
23+
}
24+
25+
func TestIssuesGranularToolset(t *testing.T) {
26+
t.Run("toolset contains expected tools", func(t *testing.T) {
27+
tools := granularToolsForToolset(ToolsetMetadataIssuesGranular.ID)
28+
29+
toolNames := make([]string, 0, len(tools))
30+
for _, tool := range tools {
31+
toolNames = append(toolNames, tool.Tool.Name)
32+
}
33+
34+
assert.Contains(t, toolNames, "create_issue")
35+
assert.Contains(t, toolNames, "update_issue_title")
36+
assert.Contains(t, toolNames, "update_issue_body")
37+
assert.Contains(t, toolNames, "update_issue_assignees")
38+
assert.Contains(t, toolNames, "update_issue_labels")
39+
assert.Contains(t, toolNames, "update_issue_milestone")
40+
assert.Contains(t, toolNames, "update_issue_type")
41+
assert.Contains(t, toolNames, "update_issue_state")
42+
assert.Contains(t, toolNames, "add_sub_issue")
43+
assert.Contains(t, toolNames, "remove_sub_issue")
44+
assert.Contains(t, toolNames, "reprioritize_sub_issue")
45+
})
46+
47+
t.Run("all tools belong to issues_granular toolset", func(t *testing.T) {
48+
tools := granularToolsForToolset(ToolsetMetadataIssuesGranular.ID)
49+
50+
for _, tool := range tools {
51+
assert.Equal(t, ToolsetMetadataIssuesGranular.ID, tool.Toolset.ID, "tool %s should belong to issues_granular toolset", tool.Tool.Name)
52+
}
53+
})
54+
55+
t.Run("all tools have ReadOnlyHint false", func(t *testing.T) {
56+
tools := granularToolsForToolset(ToolsetMetadataIssuesGranular.ID)
57+
58+
for _, tool := range tools {
59+
assert.False(t, tool.Tool.Annotations.ReadOnlyHint, "tool %s should have ReadOnlyHint=false", tool.Tool.Name)
60+
}
61+
})
62+
63+
t.Run("toolset is non-default", func(t *testing.T) {
64+
assert.False(t, ToolsetMetadataIssuesGranular.Default, "issues_granular toolset should not be default")
65+
})
66+
}
67+
68+
func TestPullRequestsGranularToolset(t *testing.T) {
69+
t.Run("toolset contains expected tools", func(t *testing.T) {
70+
tools := granularToolsForToolset(ToolsetMetadataPullRequestsGranular.ID)
71+
72+
toolNames := make([]string, 0, len(tools))
73+
for _, tool := range tools {
74+
toolNames = append(toolNames, tool.Tool.Name)
75+
}
76+
77+
assert.Contains(t, toolNames, "update_pull_request_title")
78+
assert.Contains(t, toolNames, "update_pull_request_body")
79+
assert.Contains(t, toolNames, "update_pull_request_state")
80+
assert.Contains(t, toolNames, "update_pull_request_draft_state")
81+
assert.Contains(t, toolNames, "request_pull_request_reviewers")
82+
assert.Contains(t, toolNames, "create_pull_request_review")
83+
assert.Contains(t, toolNames, "submit_pending_pull_request_review")
84+
assert.Contains(t, toolNames, "delete_pending_pull_request_review")
85+
assert.Contains(t, toolNames, "add_pull_request_review_comment")
86+
})
87+
88+
t.Run("all tools belong to pull_requests_granular toolset", func(t *testing.T) {
89+
tools := granularToolsForToolset(ToolsetMetadataPullRequestsGranular.ID)
90+
91+
for _, tool := range tools {
92+
assert.Equal(t, ToolsetMetadataPullRequestsGranular.ID, tool.Toolset.ID, "tool %s should belong to pull_requests_granular toolset", tool.Tool.Name)
93+
}
94+
})
95+
96+
t.Run("toolset is non-default", func(t *testing.T) {
97+
assert.False(t, ToolsetMetadataPullRequestsGranular.Default, "pull_requests_granular toolset should not be default")
98+
})
99+
}
100+
101+
func TestGranularCreateIssue(t *testing.T) {
102+
mockIssue := &gogithub.Issue{
103+
Number: gogithub.Ptr(1),
104+
Title: gogithub.Ptr("Test Issue"),
105+
Body: gogithub.Ptr("Test body"),
106+
}
107+
108+
tests := []struct {
109+
name string
110+
mockedClient *http.Client
111+
requestArgs map[string]any
112+
expectedErrMsg string
113+
}{
114+
{
115+
name: "successful creation",
116+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
117+
PostReposIssuesByOwnerByRepo: expectRequestBody(t, map[string]any{
118+
"title": "Test Issue",
119+
"body": "Test body",
120+
}).andThen(mockResponse(t, http.StatusCreated, mockIssue)),
121+
}),
122+
requestArgs: map[string]any{
123+
"owner": "owner",
124+
"repo": "repo",
125+
"title": "Test Issue",
126+
"body": "Test body",
127+
},
128+
},
129+
{
130+
name: "missing required parameter",
131+
mockedClient: MockHTTPClientWithHandlers(nil),
132+
requestArgs: map[string]any{
133+
"owner": "owner",
134+
"repo": "repo",
135+
},
136+
expectedErrMsg: "missing required parameter: title",
137+
},
138+
}
139+
140+
for _, tc := range tests {
141+
t.Run(tc.name, func(t *testing.T) {
142+
client := gogithub.NewClient(tc.mockedClient)
143+
deps := BaseDeps{Client: client}
144+
serverTool := GranularCreateIssue(translations.NullTranslationHelper)
145+
handler := serverTool.Handler(deps)
146+
147+
request := createMCPRequest(tc.requestArgs)
148+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
149+
require.NoError(t, err)
150+
151+
if tc.expectedErrMsg != "" {
152+
textContent := getTextResult(t, result)
153+
assert.Contains(t, textContent.Text, tc.expectedErrMsg)
154+
return
155+
}
156+
assert.False(t, result.IsError)
157+
})
158+
}
159+
}
160+
161+
func TestGranularUpdateIssueTitle(t *testing.T) {
162+
client := gogithub.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
163+
PatchReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, &gogithub.Issue{
164+
Number: gogithub.Ptr(42),
165+
Title: gogithub.Ptr("New Title"),
166+
}),
167+
}))
168+
deps := BaseDeps{Client: client}
169+
serverTool := GranularUpdateIssueTitle(translations.NullTranslationHelper)
170+
handler := serverTool.Handler(deps)
171+
172+
request := createMCPRequest(map[string]any{
173+
"owner": "owner",
174+
"repo": "repo",
175+
"issue_number": float64(42),
176+
"title": "New Title",
177+
})
178+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
179+
require.NoError(t, err)
180+
assert.False(t, result.IsError)
181+
}
182+
183+
func TestGranularUpdateIssueLabels(t *testing.T) {
184+
client := gogithub.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
185+
PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, map[string]any{
186+
"labels": []any{"bug", "enhancement"},
187+
}).andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})),
188+
}))
189+
deps := BaseDeps{Client: client}
190+
serverTool := GranularUpdateIssueLabels(translations.NullTranslationHelper)
191+
handler := serverTool.Handler(deps)
192+
193+
request := createMCPRequest(map[string]any{
194+
"owner": "owner",
195+
"repo": "repo",
196+
"issue_number": float64(1),
197+
"labels": []string{"bug", "enhancement"},
198+
})
199+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
200+
require.NoError(t, err)
201+
assert.False(t, result.IsError)
202+
}
203+
204+
func TestGranularUpdateIssueState(t *testing.T) {
205+
client := gogithub.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
206+
PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, map[string]any{
207+
"state": "closed",
208+
"state_reason": "completed",
209+
}).andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{
210+
Number: gogithub.Ptr(1),
211+
State: gogithub.Ptr("closed"),
212+
})),
213+
}))
214+
deps := BaseDeps{Client: client}
215+
serverTool := GranularUpdateIssueState(translations.NullTranslationHelper)
216+
handler := serverTool.Handler(deps)
217+
218+
request := createMCPRequest(map[string]any{
219+
"owner": "owner",
220+
"repo": "repo",
221+
"issue_number": float64(1),
222+
"state": "closed",
223+
"state_reason": "completed",
224+
})
225+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
226+
require.NoError(t, err)
227+
assert.False(t, result.IsError)
228+
}
229+
230+
func TestGranularRequestPullRequestReviewers(t *testing.T) {
231+
client := gogithub.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
232+
PostReposPullsRequestedReviewersByOwnerByRepoByPullNumber: mockResponse(t, http.StatusOK, &gogithub.PullRequest{Number: gogithub.Ptr(1)}),
233+
}))
234+
deps := BaseDeps{Client: client}
235+
serverTool := GranularRequestPullRequestReviewers(translations.NullTranslationHelper)
236+
handler := serverTool.Handler(deps)
237+
238+
request := createMCPRequest(map[string]any{
239+
"owner": "owner",
240+
"repo": "repo",
241+
"pullNumber": float64(1),
242+
"reviewers": []string{"user1", "user2"},
243+
})
244+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
245+
require.NoError(t, err)
246+
assert.False(t, result.IsError)
247+
}

0 commit comments

Comments
 (0)