Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit4d80d93

Browse files
committed
Create 'remove sub-issue' tool
1 parent8c43acf commit4d80d93

File tree

3 files changed

+368
-0
lines changed

3 files changed

+368
-0
lines changed

‎pkg/github/issues.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,109 @@ func ListSubIssues(getClient GetClientFn, t translations.TranslationHelperFunc)
364364
}
365365
}
366366

367+
// RemoveSubIssue creates a tool to remove a sub-issue from a parent issue.
368+
funcRemoveSubIssue(getClientGetClientFn,t translations.TranslationHelperFunc) (tool mcp.Tool,handler server.ToolHandlerFunc) {
369+
returnmcp.NewTool("remove_sub_issue",
370+
mcp.WithDescription(t("TOOL_REMOVE_SUB_ISSUE_DESCRIPTION","Remove a sub-issue from a parent issue in a GitHub repository.")),
371+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
372+
Title:t("TOOL_REMOVE_SUB_ISSUE_USER_TITLE","Remove sub-issue"),
373+
ReadOnlyHint:toBoolPtr(false),
374+
}),
375+
mcp.WithString("owner",
376+
mcp.Required(),
377+
mcp.Description("Repository owner"),
378+
),
379+
mcp.WithString("repo",
380+
mcp.Required(),
381+
mcp.Description("Repository name"),
382+
),
383+
mcp.WithNumber("issue_number",
384+
mcp.Required(),
385+
mcp.Description("The number of the parent issue"),
386+
),
387+
mcp.WithNumber("sub_issue_id",
388+
mcp.Required(),
389+
mcp.Description("The ID of the sub-issue to remove"),
390+
),
391+
),
392+
func(ctx context.Context,request mcp.CallToolRequest) (*mcp.CallToolResult,error) {
393+
owner,err:=requiredParam[string](request,"owner")
394+
iferr!=nil {
395+
returnmcp.NewToolResultError(err.Error()),nil
396+
}
397+
repo,err:=requiredParam[string](request,"repo")
398+
iferr!=nil {
399+
returnmcp.NewToolResultError(err.Error()),nil
400+
}
401+
issueNumber,err:=RequiredInt(request,"issue_number")
402+
iferr!=nil {
403+
returnmcp.NewToolResultError(err.Error()),nil
404+
}
405+
subIssueID,err:=RequiredInt(request,"sub_issue_id")
406+
iferr!=nil {
407+
returnmcp.NewToolResultError(err.Error()),nil
408+
}
409+
410+
client,err:=getClient(ctx)
411+
iferr!=nil {
412+
returnnil,fmt.Errorf("failed to get GitHub client: %w",err)
413+
}
414+
415+
// Create the request body
416+
requestBody:=map[string]interface{}{
417+
"sub_issue_id":subIssueID,
418+
}
419+
420+
// Since the go-github library might not have sub-issues support yet,
421+
// we'll make a direct HTTP request using the client's HTTP client
422+
reqBodyBytes,err:=json.Marshal(requestBody)
423+
iferr!=nil {
424+
returnnil,fmt.Errorf("failed to marshal request body: %w",err)
425+
}
426+
427+
url:=fmt.Sprintf("%srepos/%s/%s/issues/%d/sub_issue",
428+
client.BaseURL.String(),owner,repo,issueNumber)
429+
req,err:=http.NewRequestWithContext(ctx,"DELETE",url,strings.NewReader(string(reqBodyBytes)))
430+
iferr!=nil {
431+
returnnil,fmt.Errorf("failed to create request: %w",err)
432+
}
433+
434+
req.Header.Set("Accept","application/vnd.github+json")
435+
req.Header.Set("Content-Type","application/json")
436+
req.Header.Set("X-GitHub-Api-Version","2022-11-28")
437+
438+
// Use the same authentication as the GitHub client
439+
httpClient:=client.Client()
440+
resp,err:=httpClient.Do(req)
441+
iferr!=nil {
442+
returnnil,fmt.Errorf("failed to remove sub-issue: %w",err)
443+
}
444+
deferfunc() {_=resp.Body.Close() }()
445+
446+
body,err:=io.ReadAll(resp.Body)
447+
iferr!=nil {
448+
returnnil,fmt.Errorf("failed to read response body: %w",err)
449+
}
450+
451+
ifresp.StatusCode!=http.StatusOK {
452+
returnmcp.NewToolResultError(fmt.Sprintf("failed to remove sub-issue: %s",string(body))),nil
453+
}
454+
455+
// Parse and re-marshal to ensure consistent formatting
456+
varresultinterface{}
457+
iferr:=json.Unmarshal(body,&result);err!=nil {
458+
returnnil,fmt.Errorf("failed to unmarshal response: %w",err)
459+
}
460+
461+
r,err:=json.Marshal(result)
462+
iferr!=nil {
463+
returnnil,fmt.Errorf("failed to marshal response: %w",err)
464+
}
465+
466+
returnmcp.NewToolResultText(string(r)),nil
467+
}
468+
}
469+
367470
// SearchIssues creates a tool to search for issues and pull requests.
368471
funcSearchIssues(getClientGetClientFn,t translations.TranslationHelperFunc) (tool mcp.Tool,handler server.ToolHandlerFunc) {
369472
returnmcp.NewTool("search_issues",

‎pkg/github/issues_test.go

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2120,3 +2120,267 @@ func Test_ListSubIssues(t *testing.T) {
21202120
})
21212121
}
21222122
}
2123+
2124+
funcTest_RemoveSubIssue(t*testing.T) {
2125+
// Verify tool definition once
2126+
mockClient:=github.NewClient(nil)
2127+
tool,_:=RemoveSubIssue(stubGetClientFn(mockClient),translations.NullTranslationHelper)
2128+
2129+
assert.Equal(t,"remove_sub_issue",tool.Name)
2130+
assert.NotEmpty(t,tool.Description)
2131+
assert.Contains(t,tool.InputSchema.Properties,"owner")
2132+
assert.Contains(t,tool.InputSchema.Properties,"repo")
2133+
assert.Contains(t,tool.InputSchema.Properties,"issue_number")
2134+
assert.Contains(t,tool.InputSchema.Properties,"sub_issue_id")
2135+
assert.ElementsMatch(t,tool.InputSchema.Required, []string{"owner","repo","issue_number","sub_issue_id"})
2136+
2137+
// Setup mock issue for success case (matches GitHub API response format - the updated parent issue)
2138+
mockIssue:=&github.Issue{
2139+
Number:github.Ptr(42),
2140+
Title:github.Ptr("Parent Issue"),
2141+
Body:github.Ptr("This is the parent issue after sub-issue removal"),
2142+
State:github.Ptr("open"),
2143+
HTMLURL:github.Ptr("https://github.com/owner/repo/issues/42"),
2144+
User:&github.User{
2145+
Login:github.Ptr("testuser"),
2146+
},
2147+
Labels: []*github.Label{
2148+
{
2149+
Name:github.Ptr("enhancement"),
2150+
Color:github.Ptr("84b6eb"),
2151+
Description:github.Ptr("New feature or request"),
2152+
},
2153+
},
2154+
}
2155+
2156+
tests:= []struct {
2157+
namestring
2158+
mockedClient*http.Client
2159+
requestArgsmap[string]interface{}
2160+
expectErrorbool
2161+
expectedIssue*github.Issue
2162+
expectedErrMsgstring
2163+
}{
2164+
{
2165+
name:"successful sub-issue removal",
2166+
mockedClient:mock.NewMockedHTTPClient(
2167+
mock.WithRequestMatchHandler(
2168+
mock.EndpointPattern{
2169+
Pattern:"/repos/owner/repo/issues/42/sub_issue",
2170+
Method:"DELETE",
2171+
},
2172+
expectRequestBody(t,map[string]interface{}{
2173+
"sub_issue_id":float64(123),
2174+
}).andThen(
2175+
mockResponse(t,http.StatusOK,mockIssue),
2176+
),
2177+
),
2178+
),
2179+
requestArgs:map[string]interface{}{
2180+
"owner":"owner",
2181+
"repo":"repo",
2182+
"issue_number":float64(42),
2183+
"sub_issue_id":float64(123),
2184+
},
2185+
expectError:false,
2186+
expectedIssue:mockIssue,
2187+
},
2188+
{
2189+
name:"parent issue not found",
2190+
mockedClient:mock.NewMockedHTTPClient(
2191+
mock.WithRequestMatchHandler(
2192+
mock.EndpointPattern{
2193+
Pattern:"/repos/owner/repo/issues/999/sub_issue",
2194+
Method:"DELETE",
2195+
},
2196+
http.HandlerFunc(func(w http.ResponseWriter,_*http.Request) {
2197+
w.WriteHeader(http.StatusNotFound)
2198+
_,_=w.Write([]byte(`{"message": "Not Found"}`))
2199+
}),
2200+
),
2201+
),
2202+
requestArgs:map[string]interface{}{
2203+
"owner":"owner",
2204+
"repo":"repo",
2205+
"issue_number":float64(999),
2206+
"sub_issue_id":float64(123),
2207+
},
2208+
expectError:false,
2209+
expectedErrMsg:"failed to remove sub-issue",
2210+
},
2211+
{
2212+
name:"sub-issue not found",
2213+
mockedClient:mock.NewMockedHTTPClient(
2214+
mock.WithRequestMatchHandler(
2215+
mock.EndpointPattern{
2216+
Pattern:"/repos/owner/repo/issues/42/sub_issue",
2217+
Method:"DELETE",
2218+
},
2219+
http.HandlerFunc(func(w http.ResponseWriter,_*http.Request) {
2220+
w.WriteHeader(http.StatusNotFound)
2221+
_,_=w.Write([]byte(`{"message": "Sub-issue not found"}`))
2222+
}),
2223+
),
2224+
),
2225+
requestArgs:map[string]interface{}{
2226+
"owner":"owner",
2227+
"repo":"repo",
2228+
"issue_number":float64(42),
2229+
"sub_issue_id":float64(999),
2230+
},
2231+
expectError:false,
2232+
expectedErrMsg:"failed to remove sub-issue",
2233+
},
2234+
{
2235+
name:"bad request - invalid sub_issue_id",
2236+
mockedClient:mock.NewMockedHTTPClient(
2237+
mock.WithRequestMatchHandler(
2238+
mock.EndpointPattern{
2239+
Pattern:"/repos/owner/repo/issues/42/sub_issue",
2240+
Method:"DELETE",
2241+
},
2242+
http.HandlerFunc(func(w http.ResponseWriter,_*http.Request) {
2243+
w.WriteHeader(http.StatusBadRequest)
2244+
_,_=w.Write([]byte(`{"message": "Invalid sub_issue_id"}`))
2245+
}),
2246+
),
2247+
),
2248+
requestArgs:map[string]interface{}{
2249+
"owner":"owner",
2250+
"repo":"repo",
2251+
"issue_number":float64(42),
2252+
"sub_issue_id":float64(-1),
2253+
},
2254+
expectError:false,
2255+
expectedErrMsg:"failed to remove sub-issue",
2256+
},
2257+
{
2258+
name:"repository not found",
2259+
mockedClient:mock.NewMockedHTTPClient(
2260+
mock.WithRequestMatchHandler(
2261+
mock.EndpointPattern{
2262+
Pattern:"/repos/nonexistent/repo/issues/42/sub_issue",
2263+
Method:"DELETE",
2264+
},
2265+
http.HandlerFunc(func(w http.ResponseWriter,_*http.Request) {
2266+
w.WriteHeader(http.StatusNotFound)
2267+
_,_=w.Write([]byte(`{"message": "Not Found"}`))
2268+
}),
2269+
),
2270+
),
2271+
requestArgs:map[string]interface{}{
2272+
"owner":"nonexistent",
2273+
"repo":"repo",
2274+
"issue_number":float64(42),
2275+
"sub_issue_id":float64(123),
2276+
},
2277+
expectError:false,
2278+
expectedErrMsg:"failed to remove sub-issue",
2279+
},
2280+
{
2281+
name:"insufficient permissions",
2282+
mockedClient:mock.NewMockedHTTPClient(
2283+
mock.WithRequestMatchHandler(
2284+
mock.EndpointPattern{
2285+
Pattern:"/repos/owner/repo/issues/42/sub_issue",
2286+
Method:"DELETE",
2287+
},
2288+
http.HandlerFunc(func(w http.ResponseWriter,_*http.Request) {
2289+
w.WriteHeader(http.StatusForbidden)
2290+
_,_=w.Write([]byte(`{"message": "Must have write access to repository"}`))
2291+
}),
2292+
),
2293+
),
2294+
requestArgs:map[string]interface{}{
2295+
"owner":"owner",
2296+
"repo":"repo",
2297+
"issue_number":float64(42),
2298+
"sub_issue_id":float64(123),
2299+
},
2300+
expectError:false,
2301+
expectedErrMsg:"failed to remove sub-issue",
2302+
},
2303+
{
2304+
name:"missing required parameter owner",
2305+
mockedClient:mock.NewMockedHTTPClient(
2306+
mock.WithRequestMatchHandler(
2307+
mock.EndpointPattern{
2308+
Pattern:"/repos/owner/repo/issues/42/sub_issue",
2309+
Method:"DELETE",
2310+
},
2311+
mockResponse(t,http.StatusOK,mockIssue),
2312+
),
2313+
),
2314+
requestArgs:map[string]interface{}{
2315+
"repo":"repo",
2316+
"issue_number":float64(42),
2317+
"sub_issue_id":float64(123),
2318+
},
2319+
expectError:false,
2320+
expectedErrMsg:"missing required parameter: owner",
2321+
},
2322+
{
2323+
name:"missing required parameter sub_issue_id",
2324+
mockedClient:mock.NewMockedHTTPClient(
2325+
mock.WithRequestMatchHandler(
2326+
mock.EndpointPattern{
2327+
Pattern:"/repos/owner/repo/issues/42/sub_issue",
2328+
Method:"DELETE",
2329+
},
2330+
mockResponse(t,http.StatusOK,mockIssue),
2331+
),
2332+
),
2333+
requestArgs:map[string]interface{}{
2334+
"owner":"owner",
2335+
"repo":"repo",
2336+
"issue_number":float64(42),
2337+
},
2338+
expectError:false,
2339+
expectedErrMsg:"missing required parameter: sub_issue_id",
2340+
},
2341+
}
2342+
2343+
for_,tc:=rangetests {
2344+
t.Run(tc.name,func(t*testing.T) {
2345+
// Setup client with mock
2346+
client:=github.NewClient(tc.mockedClient)
2347+
_,handler:=RemoveSubIssue(stubGetClientFn(client),translations.NullTranslationHelper)
2348+
2349+
// Create call request
2350+
request:=createMCPRequest(tc.requestArgs)
2351+
2352+
// Call handler
2353+
result,err:=handler(context.Background(),request)
2354+
2355+
// Verify results
2356+
iftc.expectError {
2357+
require.Error(t,err)
2358+
assert.Contains(t,err.Error(),tc.expectedErrMsg)
2359+
return
2360+
}
2361+
2362+
iftc.expectedErrMsg!="" {
2363+
require.NotNil(t,result)
2364+
textContent:=getTextResult(t,result)
2365+
assert.Contains(t,textContent.Text,tc.expectedErrMsg)
2366+
return
2367+
}
2368+
2369+
require.NoError(t,err)
2370+
2371+
// Parse the result and get the text content if no error
2372+
textContent:=getTextResult(t,result)
2373+
2374+
// Unmarshal and verify the result
2375+
varreturnedIssue github.Issue
2376+
err=json.Unmarshal([]byte(textContent.Text),&returnedIssue)
2377+
require.NoError(t,err)
2378+
assert.Equal(t,*tc.expectedIssue.Number,*returnedIssue.Number)
2379+
assert.Equal(t,*tc.expectedIssue.Title,*returnedIssue.Title)
2380+
assert.Equal(t,*tc.expectedIssue.Body,*returnedIssue.Body)
2381+
assert.Equal(t,*tc.expectedIssue.State,*returnedIssue.State)
2382+
assert.Equal(t,*tc.expectedIssue.HTMLURL,*returnedIssue.HTMLURL)
2383+
assert.Equal(t,*tc.expectedIssue.User.Login,*returnedIssue.User.Login)
2384+
})
2385+
}
2386+
}

‎pkg/github/tools.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn,
5454
toolsets.NewServerTool(UpdateIssue(getClient,t)),
5555
toolsets.NewServerTool(AssignCopilotToIssue(getGQLClient,t)),
5656
toolsets.NewServerTool(AddSubIssue(getClient,t)),
57+
toolsets.NewServerTool(RemoveSubIssue(getClient,t)),
5758
)
5859
users:=toolsets.NewToolset("users","GitHub User related tools").
5960
AddReadTools(

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp