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

Add specifying state change reason toupdate_issue tool#1073

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
tonytrg merged 9 commits intomainfromkerobbi/add-state-change-reason
Sep 12, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletionsREADME.md
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -591,12 +591,14 @@ The following sets of tools are available (all are on by default):
- **update_issue** - Edit issue
- `assignees`: New assignees (string[], optional)
- `body`: New description (string, optional)
- `duplicate_of`: Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'. (number, optional)
- `issue_number`: Issue number to update (number, required)
- `labels`: New labels (string[], optional)
- `milestone`: New milestone number (number, optional)
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)
- `state`: New state (string, optional)
- `state_reason`: Reason for the state change. Ignored unless state is changed. (string, optional)
- `title`: New title (string, optional)
- `type`: New issue type (string, optional)

Expand Down
13 changes: 13 additions & 0 deletionspkg/github/__toolsnaps__/update_issue.snap
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,10 @@
"description": "New description",
"type": "string"
},
"duplicate_of": {
"description": "Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'.",
"type": "number"
},
"issue_number": {
"description": "Issue number to update",
"type": "number"
Expand DownExpand Up@@ -48,6 +52,15 @@
],
"type": "string"
},
"state_reason": {
"description": "Reason for the state change. Ignored unless state is changed.",
"enum": [
"completed",
"not_planned",
"duplicate"
],
"type": "string"
},
"title": {
"description": "New title",
"type": "string"
Expand Down
202 changes: 188 additions & 14 deletionspkg/github/issues.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,87 @@ import (
"github.com/shurcooL/githubv4"
)

// CloseIssueInput represents the input for closing an issue via the GraphQL API.
// Used to extend the functionality of the githubv4 library to support closing issues as duplicates.
type CloseIssueInput struct {
IssueID githubv4.ID `json:"issueId"`
ClientMutationID *githubv4.String `json:"clientMutationId,omitempty"`
StateReason *IssueClosedStateReason `json:"stateReason,omitempty"`
DuplicateIssueID *githubv4.ID `json:"duplicateIssueId,omitempty"`
}

// IssueClosedStateReason represents the reason an issue was closed.
// Used to extend the functionality of the githubv4 library to support closing issues as duplicates.
type IssueClosedStateReason string

const (
IssueClosedStateReasonCompleted IssueClosedStateReason = "COMPLETED"
IssueClosedStateReasonDuplicate IssueClosedStateReason = "DUPLICATE"
IssueClosedStateReasonNotPlanned IssueClosedStateReason = "NOT_PLANNED"
)

// fetchIssueIDs retrieves issue IDs via the GraphQL API.
// When duplicateOf is 0, it fetches only the main issue ID.
// When duplicateOf is non-zero, it fetches both the main issue and duplicate issue IDs in a single query.
func fetchIssueIDs(ctx context.Context, gqlClient *githubv4.Client, owner, repo string, issueNumber int, duplicateOf int) (githubv4.ID, githubv4.ID, error) {
// Build query variables common to both cases
vars := map[string]interface{}{
"owner": githubv4.String(owner),
"repo": githubv4.String(repo),
"issueNumber": githubv4.Int(issueNumber), // #nosec G115 - issue numbers are always small positive integers
}

if duplicateOf == 0 {
// Only fetch the main issue ID
var query struct {
Repository struct {
Issue struct {
ID githubv4.ID
} `graphql:"issue(number: $issueNumber)"`
} `graphql:"repository(owner: $owner, name: $repo)"`
}

if err := gqlClient.Query(ctx, &query, vars); err != nil {
return "", "", fmt.Errorf("failed to get issue ID")
}

return query.Repository.Issue.ID, "", nil
}

// Fetch both issue IDs in a single query
var query struct {
Repository struct {
Issue struct {
ID githubv4.ID
} `graphql:"issue(number: $issueNumber)"`
DuplicateIssue struct {
ID githubv4.ID
} `graphql:"duplicateIssue: issue(number: $duplicateOf)"`
} `graphql:"repository(owner: $owner, name: $repo)"`
}

// Add duplicate issue number to variables
vars["duplicateOf"] = githubv4.Int(duplicateOf) // #nosec G115 - issue numbers are always small positive integers

if err := gqlClient.Query(ctx, &query, vars); err != nil {
return "", "", fmt.Errorf("failed to get issue ID")
}

return query.Repository.Issue.ID, query.Repository.DuplicateIssue.ID, nil
}

// getCloseStateReason converts a string state reason to the appropriate enum value
func getCloseStateReason(stateReason string) IssueClosedStateReason {
switch stateReason {
case "not_planned":
return IssueClosedStateReasonNotPlanned
case "duplicate":
return IssueClosedStateReasonDuplicate
default: // Default to "completed" for empty or "completed" values
return IssueClosedStateReasonCompleted
}
}

// IssueFragment represents a fragment of an issue node in the GraphQL API.
type IssueFragment struct {
Number githubv4.Int
Expand DownExpand Up@@ -1100,7 +1181,7 @@ func ListIssues(getGQLClient GetGQLClientFn, t translations.TranslationHelperFun
}

// UpdateIssue creates a tool to update an existing issue in a GitHub repository.
func UpdateIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
func UpdateIssue(getClient GetClientFn,getGQLClient GetGQLClientFn,t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("update_issue",
mcp.WithDescription(t("TOOL_UPDATE_ISSUE_DESCRIPTION", "Update an existing issue in a GitHub repository.")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Expand All@@ -1125,10 +1206,6 @@ func UpdateIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (t
mcp.WithString("body",
mcp.Description("New description"),
),
mcp.WithString("state",
mcp.Description("New state"),
mcp.Enum("open", "closed"),
),
mcp.WithArray("labels",
mcp.Description("New labels"),
mcp.Items(
Expand All@@ -1151,6 +1228,17 @@ func UpdateIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (t
mcp.WithString("type",
mcp.Description("New issue type"),
),
mcp.WithString("state",
mcp.Description("New state"),
mcp.Enum("open", "closed"),
),
mcp.WithString("state_reason",
mcp.Description("Reason for the state change. Ignored unless state is changed."),
mcp.Enum("completed", "not_planned", "duplicate"),
),
mcp.WithNumber("duplicate_of",
mcp.Description("Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'."),
),
),
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
owner, err := RequiredParam[string](request, "owner")
Expand DownExpand Up@@ -1186,14 +1274,6 @@ func UpdateIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (t
issueRequest.Body = github.Ptr(body)
}

state, err := OptionalParam[string](request, "state")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
if state != "" {
issueRequest.State = github.Ptr(state)
}

// Get labels
labels, err := OptionalStringArrayParam(request, "labels")
if err != nil {
Expand DownExpand Up@@ -1230,13 +1310,38 @@ func UpdateIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (t
issueRequest.Type = github.Ptr(issueType)
}

// Handle state, state_reason and duplicateOf parameters
state, err := OptionalParam[string](request, "state")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

stateReason, err := OptionalParam[string](request, "state_reason")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

duplicateOf, err := OptionalIntParam(request, "duplicate_of")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
if duplicateOf != 0 && stateReason != "duplicate" {
return mcp.NewToolResultError("duplicate_of can only be used when state_reason is 'duplicate'"), nil
}

// Use REST API for non-state updates
client, err := getClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
}

updatedIssue, resp, err := client.Issues.Edit(ctx, owner, repo, issueNumber, issueRequest)
if err != nil {
return nil, fmt.Errorf("failed to update issue: %w", err)
return ghErrors.NewGitHubAPIErrorResponse(ctx,
"failed to update issue",
resp,
err,
), nil
}
defer func() { _ = resp.Body.Close() }()

Expand All@@ -1248,6 +1353,75 @@ func UpdateIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (t
return mcp.NewToolResultError(fmt.Sprintf("failed to update issue: %s", string(body))), nil
}

// Use GraphQL API for state updates
if state != "" {
gqlClient, err := getGQLClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get GraphQL client: %w", err)
}

// Mandate specifying duplicateOf when trying to close as duplicate
if state == "closed" && stateReason == "duplicate" && duplicateOf == 0 {
return mcp.NewToolResultError("duplicate_of must be provided when state_reason is 'duplicate'"), nil
}

// Get target issue ID (and duplicate issue ID if needed)
issueID, duplicateIssueID, err := fetchIssueIDs(ctx, gqlClient, owner, repo, issueNumber, duplicateOf)
if err != nil {
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find issues", err), nil
}

switch state {
case "open":
// Use ReopenIssue mutation for opening
var mutation struct {
ReopenIssue struct {
Issue struct {
ID githubv4.ID
Number githubv4.Int
URL githubv4.String
State githubv4.String
}
} `graphql:"reopenIssue(input: $input)"`
}

err = gqlClient.Mutate(ctx, &mutation, githubv4.ReopenIssueInput{
IssueID: issueID,
}, nil)
if err != nil {
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to reopen issue", err), nil
}
case "closed":
// Use CloseIssue mutation for closing
var mutation struct {
CloseIssue struct {
Issue struct {
ID githubv4.ID
Number githubv4.Int
URL githubv4.String
State githubv4.String
}
} `graphql:"closeIssue(input: $input)"`
}

stateReasonValue := getCloseStateReason(stateReason)
closeInput := CloseIssueInput{
IssueID: issueID,
StateReason: &stateReasonValue,
}

// Set duplicate issue ID if needed
if stateReason == "duplicate" {
closeInput.DuplicateIssueID = &duplicateIssueID
}

err = gqlClient.Mutate(ctx, &mutation, closeInput, nil)
if err != nil {
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to close issue", err), nil
}
}
}

// Return minimal response with just essential information
minimalResponse := MinimalResponse{
ID: fmt.Sprintf("%d", updatedIssue.GetID()),
Expand Down
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp