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

Commit5f5a0a5

Browse files
committed
add support for the update_issue tool
1 parent14fb02b commit5f5a0a5

File tree

4 files changed

+305
-2
lines changed

4 files changed

+305
-2
lines changed

‎README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@ and set it as the GITHUB_PERSONAL_ACCESS_TOKEN environment variable.
5050
-`page`: Page number (number, optional)
5151
-`per_page`: Results per page (number, optional)
5252

53+
-**update_issue** - Update an existing issue in a GitHub repository
54+
55+
-`owner`: Repository owner (string, required)
56+
-`repo`: Repository name (string, required)
57+
-`issue_number`: Issue number to update (number, required)
58+
-`title`: New title (string, optional)
59+
-`body`: New description (string, optional)
60+
-`state`: New state ('open' or 'closed') (string, optional)
61+
-`labels`: Comma-separated list of new labels (string, optional)
62+
-`assignees`: Comma-separated list of new assignees (string, optional)
63+
-`milestone`: New milestone number (number, optional)
64+
5365
-**search_issues** - Search for issues and pull requests
5466
-`query`: Search query (string, required)
5567
-`sort`: Sort field (string, optional)
@@ -368,8 +380,6 @@ Lots of things!
368380
Missing tools:
369381
370382
- push_files (files array)
371-
- list_issues (labels array)
372-
- update_issue (labels and assignees arrays)
373383
- create_pull_request_review (comments array)
374384
375385
Testing

‎pkg/github/issues.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,100 @@ func listIssues(client *github.Client, t translations.TranslationHelperFunc) (to
361361
}
362362
}
363363

364+
// updateIssue creates a tool to update an existing issue in a GitHub repository.
365+
funcupdateIssue(client*github.Client,t translations.TranslationHelperFunc) (tool mcp.Tool,handler server.ToolHandlerFunc) {
366+
returnmcp.NewTool("update_issue",
367+
mcp.WithDescription(t("TOOL_UPDATE_ISSUE_DESCRIPTION","Update an existing issue in a GitHub repository")),
368+
mcp.WithString("owner",
369+
mcp.Required(),
370+
mcp.Description("Repository owner"),
371+
),
372+
mcp.WithString("repo",
373+
mcp.Required(),
374+
mcp.Description("Repository name"),
375+
),
376+
mcp.WithNumber("issue_number",
377+
mcp.Required(),
378+
mcp.Description("Issue number to update"),
379+
),
380+
mcp.WithString("title",
381+
mcp.Description("New title"),
382+
),
383+
mcp.WithString("body",
384+
mcp.Description("New description"),
385+
),
386+
mcp.WithString("state",
387+
mcp.Description("New state ('open' or 'closed')"),
388+
),
389+
mcp.WithString("labels",
390+
mcp.Description("Comma-separated list of new labels"),
391+
),
392+
mcp.WithString("assignees",
393+
mcp.Description("Comma-separated list of new assignees"),
394+
),
395+
mcp.WithNumber("milestone",
396+
mcp.Description("New milestone number"),
397+
),
398+
),
399+
func(ctx context.Context,request mcp.CallToolRequest) (*mcp.CallToolResult,error) {
400+
owner:=request.Params.Arguments["owner"].(string)
401+
repo:=request.Params.Arguments["repo"].(string)
402+
issueNumber:=int(request.Params.Arguments["issue_number"].(float64))
403+
404+
// Create the issue request with only provided fields
405+
issueRequest:=&github.IssueRequest{}
406+
407+
// Set optional parameters if provided
408+
iftitle,ok:=request.Params.Arguments["title"].(string);ok&&title!="" {
409+
issueRequest.Title=github.Ptr(title)
410+
}
411+
412+
ifbody,ok:=request.Params.Arguments["body"].(string);ok&&body!="" {
413+
issueRequest.Body=github.Ptr(body)
414+
}
415+
416+
ifstate,ok:=request.Params.Arguments["state"].(string);ok&&state!="" {
417+
issueRequest.State=github.Ptr(state)
418+
}
419+
420+
iflabels,ok:=request.Params.Arguments["labels"].(string);ok&&labels!="" {
421+
labelsList:=parseCommaSeparatedList(labels)
422+
issueRequest.Labels=&labelsList
423+
}
424+
425+
ifassignees,ok:=request.Params.Arguments["assignees"].(string);ok&&assignees!="" {
426+
assigneesList:=parseCommaSeparatedList(assignees)
427+
issueRequest.Assignees=&assigneesList
428+
}
429+
430+
ifmilestone,ok:=request.Params.Arguments["milestone"].(float64);ok {
431+
milestoneNum:=int(milestone)
432+
issueRequest.Milestone=&milestoneNum
433+
}
434+
435+
updatedIssue,resp,err:=client.Issues.Edit(ctx,owner,repo,issueNumber,issueRequest)
436+
iferr!=nil {
437+
returnnil,fmt.Errorf("failed to update issue: %w",err)
438+
}
439+
deferfunc() {_=resp.Body.Close() }()
440+
441+
ifresp.StatusCode!=http.StatusOK {
442+
body,err:=io.ReadAll(resp.Body)
443+
iferr!=nil {
444+
returnnil,fmt.Errorf("failed to read response body: %w",err)
445+
}
446+
returnmcp.NewToolResultError(fmt.Sprintf("failed to update issue: %s",string(body))),nil
447+
}
448+
449+
r,err:=json.Marshal(updatedIssue)
450+
iferr!=nil {
451+
returnnil,fmt.Errorf("failed to marshal response: %w",err)
452+
}
453+
454+
returnmcp.NewToolResultText(string(r)),nil
455+
}
456+
}
457+
364458
// parseISOTimestamp parses an ISO 8601 timestamp string into a time.Time object.
365459
// Returns the parsed time or an error if parsing fails.
366460
// Example formats supported: "2023-01-15T14:30:00Z", "2023-01-15"

‎pkg/github/issues_test.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,204 @@ func Test_ListIssues(t *testing.T) {
693693
}
694694
}
695695

696+
funcTest_UpdateIssue(t*testing.T) {
697+
// Verify tool definition
698+
mockClient:=github.NewClient(nil)
699+
tool,_:=updateIssue(mockClient,translations.NullTranslationHelper)
700+
701+
assert.Equal(t,"update_issue",tool.Name)
702+
assert.NotEmpty(t,tool.Description)
703+
assert.Contains(t,tool.InputSchema.Properties,"owner")
704+
assert.Contains(t,tool.InputSchema.Properties,"repo")
705+
assert.Contains(t,tool.InputSchema.Properties,"issue_number")
706+
assert.Contains(t,tool.InputSchema.Properties,"title")
707+
assert.Contains(t,tool.InputSchema.Properties,"body")
708+
assert.Contains(t,tool.InputSchema.Properties,"state")
709+
assert.Contains(t,tool.InputSchema.Properties,"labels")
710+
assert.Contains(t,tool.InputSchema.Properties,"assignees")
711+
assert.Contains(t,tool.InputSchema.Properties,"milestone")
712+
assert.ElementsMatch(t,tool.InputSchema.Required, []string{"owner","repo","issue_number"})
713+
714+
// Setup mock issue for success case
715+
mockIssue:=&github.Issue{
716+
Number:github.Ptr(123),
717+
Title:github.Ptr("Updated Issue Title"),
718+
Body:github.Ptr("Updated issue description"),
719+
State:github.Ptr("closed"),
720+
HTMLURL:github.Ptr("https://github.com/owner/repo/issues/123"),
721+
Assignees: []*github.User{{Login:github.Ptr("assignee1")}, {Login:github.Ptr("assignee2")}},
722+
Labels: []*github.Label{{Name:github.Ptr("bug")}, {Name:github.Ptr("priority")}},
723+
Milestone:&github.Milestone{Number:github.Ptr(5)},
724+
}
725+
726+
tests:= []struct {
727+
namestring
728+
mockedClient*http.Client
729+
requestArgsmap[string]interface{}
730+
expectErrorbool
731+
expectedIssue*github.Issue
732+
expectedErrMsgstring
733+
}{
734+
{
735+
name:"update issue with all fields",
736+
mockedClient:mock.NewMockedHTTPClient(
737+
mock.WithRequestMatchHandler(
738+
mock.PatchReposIssuesByOwnerByRepoByIssueNumber,
739+
mockResponse(t,http.StatusOK,mockIssue),
740+
),
741+
),
742+
requestArgs:map[string]interface{}{
743+
"owner":"owner",
744+
"repo":"repo",
745+
"issue_number":float64(123),
746+
"title":"Updated Issue Title",
747+
"body":"Updated issue description",
748+
"state":"closed",
749+
"labels":"bug,priority",
750+
"assignees":"assignee1,assignee2",
751+
"milestone":float64(5),
752+
},
753+
expectError:false,
754+
expectedIssue:mockIssue,
755+
},
756+
{
757+
name:"update issue with minimal fields",
758+
mockedClient:mock.NewMockedHTTPClient(
759+
mock.WithRequestMatchHandler(
760+
mock.PatchReposIssuesByOwnerByRepoByIssueNumber,
761+
mockResponse(t,http.StatusOK,&github.Issue{
762+
Number:github.Ptr(123),
763+
Title:github.Ptr("Only Title Updated"),
764+
HTMLURL:github.Ptr("https://github.com/owner/repo/issues/123"),
765+
State:github.Ptr("open"),
766+
}),
767+
),
768+
),
769+
requestArgs:map[string]interface{}{
770+
"owner":"owner",
771+
"repo":"repo",
772+
"issue_number":float64(123),
773+
"title":"Only Title Updated",
774+
},
775+
expectError:false,
776+
expectedIssue:&github.Issue{
777+
Number:github.Ptr(123),
778+
Title:github.Ptr("Only Title Updated"),
779+
HTMLURL:github.Ptr("https://github.com/owner/repo/issues/123"),
780+
State:github.Ptr("open"),
781+
},
782+
},
783+
{
784+
name:"update issue fails with not found",
785+
mockedClient:mock.NewMockedHTTPClient(
786+
mock.WithRequestMatchHandler(
787+
mock.PatchReposIssuesByOwnerByRepoByIssueNumber,
788+
http.HandlerFunc(func(w http.ResponseWriter,r*http.Request) {
789+
w.WriteHeader(http.StatusNotFound)
790+
_,_=w.Write([]byte(`{"message": "Issue not found"}`))
791+
}),
792+
),
793+
),
794+
requestArgs:map[string]interface{}{
795+
"owner":"owner",
796+
"repo":"repo",
797+
"issue_number":float64(999),
798+
"title":"This issue doesn't exist",
799+
},
800+
expectError:true,
801+
expectedErrMsg:"failed to update issue",
802+
},
803+
{
804+
name:"update issue fails with validation error",
805+
mockedClient:mock.NewMockedHTTPClient(
806+
mock.WithRequestMatchHandler(
807+
mock.PatchReposIssuesByOwnerByRepoByIssueNumber,
808+
http.HandlerFunc(func(w http.ResponseWriter,r*http.Request) {
809+
w.WriteHeader(http.StatusUnprocessableEntity)
810+
_,_=w.Write([]byte(`{"message": "Invalid state value"}`))
811+
}),
812+
),
813+
),
814+
requestArgs:map[string]interface{}{
815+
"owner":"owner",
816+
"repo":"repo",
817+
"issue_number":float64(123),
818+
"state":"invalid_state",
819+
},
820+
expectError:true,
821+
expectedErrMsg:"failed to update issue",
822+
},
823+
}
824+
825+
for_,tc:=rangetests {
826+
t.Run(tc.name,func(t*testing.T) {
827+
// Setup client with mock
828+
client:=github.NewClient(tc.mockedClient)
829+
_,handler:=updateIssue(client,translations.NullTranslationHelper)
830+
831+
// Create call request
832+
request:=createMCPRequest(tc.requestArgs)
833+
834+
// Call handler
835+
result,err:=handler(context.Background(),request)
836+
837+
// Verify results
838+
iftc.expectError {
839+
iferr!=nil {
840+
assert.Contains(t,err.Error(),tc.expectedErrMsg)
841+
}else {
842+
// For errors returned as part of the result, not as an error
843+
require.NotNil(t,result)
844+
textContent:=getTextResult(t,result)
845+
assert.Contains(t,textContent.Text,tc.expectedErrMsg)
846+
}
847+
return
848+
}
849+
850+
require.NoError(t,err)
851+
852+
// Parse the result and get the text content if no error
853+
textContent:=getTextResult(t,result)
854+
855+
// Unmarshal and verify the result
856+
varreturnedIssue github.Issue
857+
err=json.Unmarshal([]byte(textContent.Text),&returnedIssue)
858+
require.NoError(t,err)
859+
860+
assert.Equal(t,*tc.expectedIssue.Number,*returnedIssue.Number)
861+
assert.Equal(t,*tc.expectedIssue.Title,*returnedIssue.Title)
862+
assert.Equal(t,*tc.expectedIssue.State,*returnedIssue.State)
863+
assert.Equal(t,*tc.expectedIssue.HTMLURL,*returnedIssue.HTMLURL)
864+
865+
iftc.expectedIssue.Body!=nil {
866+
assert.Equal(t,*tc.expectedIssue.Body,*returnedIssue.Body)
867+
}
868+
869+
// Check assignees if expected
870+
iftc.expectedIssue.Assignees!=nil&&len(tc.expectedIssue.Assignees)>0 {
871+
assert.Len(t,returnedIssue.Assignees,len(tc.expectedIssue.Assignees))
872+
fori,assignee:=rangereturnedIssue.Assignees {
873+
assert.Equal(t,*tc.expectedIssue.Assignees[i].Login,*assignee.Login)
874+
}
875+
}
876+
877+
// Check labels if expected
878+
iftc.expectedIssue.Labels!=nil&&len(tc.expectedIssue.Labels)>0 {
879+
assert.Len(t,returnedIssue.Labels,len(tc.expectedIssue.Labels))
880+
fori,label:=rangereturnedIssue.Labels {
881+
assert.Equal(t,*tc.expectedIssue.Labels[i].Name,*label.Name)
882+
}
883+
}
884+
885+
// Check milestone if expected
886+
iftc.expectedIssue.Milestone!=nil {
887+
assert.NotNil(t,returnedIssue.Milestone)
888+
assert.Equal(t,*tc.expectedIssue.Milestone.Number,*returnedIssue.Milestone.Number)
889+
}
890+
})
891+
}
892+
}
893+
696894
funcTest_ParseISOTimestamp(t*testing.T) {
697895
tests:= []struct {
698896
namestring

‎pkg/github/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ func NewServer(client *github.Client, readOnly bool, t translations.TranslationH
4141
s.AddTool(createIssue(client,t))
4242
s.AddTool(addIssueComment(client,t))
4343
s.AddTool(createIssue(client,t))
44+
s.AddTool(updateIssue(client,t))
4445
}
4546

4647
// Add GitHub tools - Pull Requests

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp