Skip to content
Merged
Prev Previous commit
Next Next commit
refactor: inline IssueWrite/LegacyIssueWrite as full duplicates
Replace the shared buildIssueWrite(includeIssueFields) helper with two
fully duplicated tool definitions. When the FeatureFlagIssueFields flag
is retired, LegacyIssueWrite can be deleted as a single function with no
merge thinking required.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
  • Loading branch information
SamMorrowDrums and Copilot committed May 27, 2026
commit 39dfc2fff01a970eaff00f25571df529ab653238
258 changes: 228 additions & 30 deletions pkg/github/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -1763,27 +1763,10 @@ const IssueWriteUIResourceURI = "ui://github-mcp-server/issue-write"
// IssueWrite is the FeatureFlagIssueFields-enabled variant of issue_write
// (with the issue_fields parameter). LegacyIssueWrite is served when the flag
// is off. Both register under the tool name "issue_write"; exactly one is
// active at a time via mutually exclusive feature-flag annotations. Delete the
// LegacyIssueWrite block (and the includeIssueFields parameter) when the flag
// is removed.
// active at a time via mutually exclusive feature-flag annotations. When the
// flag is removed, delete LegacyIssueWrite outright and drop the feature-flag
// fields on IssueWrite.
func IssueWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
st := buildIssueWrite(t, true)
st.FeatureFlagEnable = FeatureFlagIssueFields
st.FeatureFlagDisable = []string{FeatureFlagIssuesGranular}
return st
}

// LegacyIssueWrite is the FeatureFlagIssueFields-disabled variant of issue_write.
// It exposes the pre-issue-fields schema (no issue_fields parameter) and skips
// the custom field value resolution. Hidden whenever the granular toolset or
// the issue-fields flag is on.
func LegacyIssueWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
st := buildIssueWrite(t, false)
st.FeatureFlagDisable = []string{FeatureFlagIssuesGranular, FeatureFlagIssueFields}
return st
}

func buildIssueWrite(t translations.TranslationHelperFunc, includeIssueFields bool) inventory.ServerTool {
st := NewTool(
ToolsetMetadataIssues,
mcp.Tool{
Expand Down Expand Up @@ -2004,11 +1987,9 @@ Options are:
}

var issueFields []issueWriteFieldInput
if includeIssueFields {
issueFields, err = optionalIssueWriteFields(args)
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
issueFields, err = optionalIssueWriteFields(args)
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}

client, err := deps.GetClient(ctx)
Expand Down Expand Up @@ -2045,11 +2026,228 @@ Options are:
return utils.NewToolResultError("invalid method, must be either 'create' or 'update'"), nil, nil
}
})
if !includeIssueFields {
if schema, ok := st.Tool.InputSchema.(*jsonschema.Schema); ok {
delete(schema.Properties, "issue_fields")
}
}
st.FeatureFlagEnable = FeatureFlagIssueFields

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to add this flag to set_issue_fields as well? It currently is still available when issues_granular is enabled. Does that need to also have the remote_mcp_issue_fields flag enabled, so that issue field writes are consistently behind the same flag?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issues granular I'm ok with having it alreayd.

st.FeatureFlagDisable = []string{FeatureFlagIssuesGranular}
return st
}

// LegacyIssueWrite is the FeatureFlagIssueFields-disabled variant of issue_write.
// It is a near-verbatim copy of IssueWrite minus the issue_fields schema
// property, the issue_fields handler block, and the related GraphQL field
// resolution. Kept as a full duplicate so removing the FeatureFlagIssueFields
// flag is a single-function delete. Hidden whenever the granular toolset or
// the issue-fields flag is on.
func LegacyIssueWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
st := NewTool(
ToolsetMetadataIssues,
mcp.Tool{
Name: "issue_write",
Description: t("TOOL_ISSUE_WRITE_DESCRIPTION", "Create a new or update an existing issue in a GitHub repository."),
Annotations: &mcp.ToolAnnotations{
Title: t("TOOL_ISSUE_WRITE_USER_TITLE", "Create or update issue"),
ReadOnlyHint: false,
},
Meta: mcp.Meta{
"ui": map[string]any{
"resourceUri": IssueWriteUIResourceURI,
"visibility": []string{"model", "app"},
},
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"method": {
Type: "string",
Description: `Write operation to perform on a single issue.
Options are:
- 'create' - creates a new issue.
- 'update' - updates an existing issue.
`,
Enum: []any{"create", "update"},
},
"owner": {
Type: "string",
Description: "Repository owner",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"issue_number": {
Type: "number",
Description: "Issue number to update",
},
"title": {
Type: "string",
Description: "Issue title",
},
"body": {
Type: "string",
Description: "Issue body content",
},
"assignees": {
Type: "array",
Description: "Usernames to assign to this issue",
Items: &jsonschema.Schema{
Type: "string",
},
},
"labels": {
Type: "array",
Description: "Labels to apply to this issue",
Items: &jsonschema.Schema{
Type: "string",
},
},
"milestone": {
Type: "number",
Description: "Milestone number",
},
"type": {
Type: "string",
Description: "Type of this issue. Only use if the repository has issue types configured. Use list_issue_types tool to get valid type values for the organization. If the repository doesn't support issue types, omit this parameter.",
},
"state": {
Type: "string",
Description: "New state",
Enum: []any{"open", "closed"},
},
"state_reason": {
Type: "string",
Description: "Reason for the state change. Ignored unless state is changed.",
Enum: []any{"completed", "not_planned", "duplicate"},
},
"duplicate_of": {
Type: "number",
Description: "Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'.",
},
},
Required: []string{"method", "owner", "repo"},
},
},
[]scopes.Scope{scopes.Repo},
func(ctx context.Context, deps ToolDependencies, req *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
method, err := RequiredParam[string](args, "method")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}

owner, err := RequiredParam[string](args, "owner")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
repo, err := RequiredParam[string](args, "repo")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}

// When MCP Apps are enabled and the client supports UI,
// check if this is a UI form submission. The UI sends _ui_submitted=true
// to distinguish form submissions from LLM calls.
uiSubmitted, _ := OptionalParam[bool](args, "_ui_submitted")

if deps.IsFeatureEnabled(ctx, MCPAppsFeatureFlag) && clientSupportsUI(ctx, req) && !uiSubmitted {
if method == "update" {
// Skip the UI form when a state change is requested because
// the form only handles title/body editing and would lose the
// state transition (e.g. closing or reopening the issue).
if _, hasState := args["state"]; !hasState {
issueNumber, numErr := RequiredInt(args, "issue_number")
if numErr != nil {
return utils.NewToolResultError("issue_number is required for update method"), nil, nil
}
return utils.NewToolResultText(fmt.Sprintf("Ready to update issue #%d in %s/%s. IMPORTANT: The issue has NOT been updated yet. Do NOT tell the user the issue was updated. The user MUST click Submit in the form to update it.", issueNumber, owner, repo)), nil, nil
}
} else {
return utils.NewToolResultText(fmt.Sprintf("Ready to create an issue in %s/%s. IMPORTANT: The issue has NOT been created yet. Do NOT tell the user the issue was created. The user MUST click Submit in the form to create it.", owner, repo)), nil, nil
}
}

title, err := OptionalParam[string](args, "title")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}

// Optional parameters
body, err := OptionalParam[string](args, "body")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}

// Get assignees
assignees, err := OptionalStringArrayParam(args, "assignees")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}

// Get labels
labels, err := OptionalStringArrayParam(args, "labels")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}

// Get optional milestone
milestone, err := OptionalIntParam(args, "milestone")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}

var milestoneNum int
if milestone != 0 {
milestoneNum = milestone
}

// Get optional type
issueType, err := OptionalParam[string](args, "type")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}

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

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

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

client, err := deps.GetClient(ctx)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
}

gqlClient, err := deps.GetGQLClient(ctx)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to get GraphQL client", err), nil, nil
}

switch method {
case "create":
result, err := CreateIssue(ctx, client, owner, repo, title, body, assignees, labels, milestoneNum, issueType, nil)
return result, nil, err
case "update":
issueNumber, err := RequiredInt(args, "issue_number")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
result, err := UpdateIssue(ctx, client, gqlClient, owner, repo, issueNumber, title, body, assignees, labels, milestoneNum, issueType, nil, nil, state, stateReason, duplicateOf)
return result, nil, err
default:
return utils.NewToolResultError("invalid method, must be either 'create' or 'update'"), nil, nil
}
})
st.FeatureFlagDisable = []string{FeatureFlagIssuesGranular, FeatureFlagIssueFields}
return st
}

Expand Down
Loading