Skip to content

Commit 39dfc2f

Browse files
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>
1 parent eacc281 commit 39dfc2f

1 file changed

Lines changed: 228 additions & 30 deletions

File tree

pkg/github/issues.go

Lines changed: 228 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1763,27 +1763,10 @@ const IssueWriteUIResourceURI = "ui://github-mcp-server/issue-write"
17631763
// IssueWrite is the FeatureFlagIssueFields-enabled variant of issue_write
17641764
// (with the issue_fields parameter). LegacyIssueWrite is served when the flag
17651765
// is off. Both register under the tool name "issue_write"; exactly one is
1766-
// active at a time via mutually exclusive feature-flag annotations. Delete the
1767-
// LegacyIssueWrite block (and the includeIssueFields parameter) when the flag
1768-
// is removed.
1766+
// active at a time via mutually exclusive feature-flag annotations. When the
1767+
// flag is removed, delete LegacyIssueWrite outright and drop the feature-flag
1768+
// fields on IssueWrite.
17691769
func IssueWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
1770-
st := buildIssueWrite(t, true)
1771-
st.FeatureFlagEnable = FeatureFlagIssueFields
1772-
st.FeatureFlagDisable = []string{FeatureFlagIssuesGranular}
1773-
return st
1774-
}
1775-
1776-
// LegacyIssueWrite is the FeatureFlagIssueFields-disabled variant of issue_write.
1777-
// It exposes the pre-issue-fields schema (no issue_fields parameter) and skips
1778-
// the custom field value resolution. Hidden whenever the granular toolset or
1779-
// the issue-fields flag is on.
1780-
func LegacyIssueWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
1781-
st := buildIssueWrite(t, false)
1782-
st.FeatureFlagDisable = []string{FeatureFlagIssuesGranular, FeatureFlagIssueFields}
1783-
return st
1784-
}
1785-
1786-
func buildIssueWrite(t translations.TranslationHelperFunc, includeIssueFields bool) inventory.ServerTool {
17871770
st := NewTool(
17881771
ToolsetMetadataIssues,
17891772
mcp.Tool{
@@ -2004,11 +1987,9 @@ Options are:
20041987
}
20051988

20061989
var issueFields []issueWriteFieldInput
2007-
if includeIssueFields {
2008-
issueFields, err = optionalIssueWriteFields(args)
2009-
if err != nil {
2010-
return utils.NewToolResultError(err.Error()), nil, nil
2011-
}
1990+
issueFields, err = optionalIssueWriteFields(args)
1991+
if err != nil {
1992+
return utils.NewToolResultError(err.Error()), nil, nil
20121993
}
20131994

20141995
client, err := deps.GetClient(ctx)
@@ -2045,11 +2026,228 @@ Options are:
20452026
return utils.NewToolResultError("invalid method, must be either 'create' or 'update'"), nil, nil
20462027
}
20472028
})
2048-
if !includeIssueFields {
2049-
if schema, ok := st.Tool.InputSchema.(*jsonschema.Schema); ok {
2050-
delete(schema.Properties, "issue_fields")
2051-
}
2052-
}
2029+
st.FeatureFlagEnable = FeatureFlagIssueFields
2030+
st.FeatureFlagDisable = []string{FeatureFlagIssuesGranular}
2031+
return st
2032+
}
2033+
2034+
// LegacyIssueWrite is the FeatureFlagIssueFields-disabled variant of issue_write.
2035+
// It is a near-verbatim copy of IssueWrite minus the issue_fields schema
2036+
// property, the issue_fields handler block, and the related GraphQL field
2037+
// resolution. Kept as a full duplicate so removing the FeatureFlagIssueFields
2038+
// flag is a single-function delete. Hidden whenever the granular toolset or
2039+
// the issue-fields flag is on.
2040+
func LegacyIssueWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
2041+
st := NewTool(
2042+
ToolsetMetadataIssues,
2043+
mcp.Tool{
2044+
Name: "issue_write",
2045+
Description: t("TOOL_ISSUE_WRITE_DESCRIPTION", "Create a new or update an existing issue in a GitHub repository."),
2046+
Annotations: &mcp.ToolAnnotations{
2047+
Title: t("TOOL_ISSUE_WRITE_USER_TITLE", "Create or update issue"),
2048+
ReadOnlyHint: false,
2049+
},
2050+
Meta: mcp.Meta{
2051+
"ui": map[string]any{
2052+
"resourceUri": IssueWriteUIResourceURI,
2053+
"visibility": []string{"model", "app"},
2054+
},
2055+
},
2056+
InputSchema: &jsonschema.Schema{
2057+
Type: "object",
2058+
Properties: map[string]*jsonschema.Schema{
2059+
"method": {
2060+
Type: "string",
2061+
Description: `Write operation to perform on a single issue.
2062+
Options are:
2063+
- 'create' - creates a new issue.
2064+
- 'update' - updates an existing issue.
2065+
`,
2066+
Enum: []any{"create", "update"},
2067+
},
2068+
"owner": {
2069+
Type: "string",
2070+
Description: "Repository owner",
2071+
},
2072+
"repo": {
2073+
Type: "string",
2074+
Description: "Repository name",
2075+
},
2076+
"issue_number": {
2077+
Type: "number",
2078+
Description: "Issue number to update",
2079+
},
2080+
"title": {
2081+
Type: "string",
2082+
Description: "Issue title",
2083+
},
2084+
"body": {
2085+
Type: "string",
2086+
Description: "Issue body content",
2087+
},
2088+
"assignees": {
2089+
Type: "array",
2090+
Description: "Usernames to assign to this issue",
2091+
Items: &jsonschema.Schema{
2092+
Type: "string",
2093+
},
2094+
},
2095+
"labels": {
2096+
Type: "array",
2097+
Description: "Labels to apply to this issue",
2098+
Items: &jsonschema.Schema{
2099+
Type: "string",
2100+
},
2101+
},
2102+
"milestone": {
2103+
Type: "number",
2104+
Description: "Milestone number",
2105+
},
2106+
"type": {
2107+
Type: "string",
2108+
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.",
2109+
},
2110+
"state": {
2111+
Type: "string",
2112+
Description: "New state",
2113+
Enum: []any{"open", "closed"},
2114+
},
2115+
"state_reason": {
2116+
Type: "string",
2117+
Description: "Reason for the state change. Ignored unless state is changed.",
2118+
Enum: []any{"completed", "not_planned", "duplicate"},
2119+
},
2120+
"duplicate_of": {
2121+
Type: "number",
2122+
Description: "Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'.",
2123+
},
2124+
},
2125+
Required: []string{"method", "owner", "repo"},
2126+
},
2127+
},
2128+
[]scopes.Scope{scopes.Repo},
2129+
func(ctx context.Context, deps ToolDependencies, req *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
2130+
method, err := RequiredParam[string](args, "method")
2131+
if err != nil {
2132+
return utils.NewToolResultError(err.Error()), nil, nil
2133+
}
2134+
2135+
owner, err := RequiredParam[string](args, "owner")
2136+
if err != nil {
2137+
return utils.NewToolResultError(err.Error()), nil, nil
2138+
}
2139+
repo, err := RequiredParam[string](args, "repo")
2140+
if err != nil {
2141+
return utils.NewToolResultError(err.Error()), nil, nil
2142+
}
2143+
2144+
// When MCP Apps are enabled and the client supports UI,
2145+
// check if this is a UI form submission. The UI sends _ui_submitted=true
2146+
// to distinguish form submissions from LLM calls.
2147+
uiSubmitted, _ := OptionalParam[bool](args, "_ui_submitted")
2148+
2149+
if deps.IsFeatureEnabled(ctx, MCPAppsFeatureFlag) && clientSupportsUI(ctx, req) && !uiSubmitted {
2150+
if method == "update" {
2151+
// Skip the UI form when a state change is requested because
2152+
// the form only handles title/body editing and would lose the
2153+
// state transition (e.g. closing or reopening the issue).
2154+
if _, hasState := args["state"]; !hasState {
2155+
issueNumber, numErr := RequiredInt(args, "issue_number")
2156+
if numErr != nil {
2157+
return utils.NewToolResultError("issue_number is required for update method"), nil, nil
2158+
}
2159+
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
2160+
}
2161+
} else {
2162+
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
2163+
}
2164+
}
2165+
2166+
title, err := OptionalParam[string](args, "title")
2167+
if err != nil {
2168+
return utils.NewToolResultError(err.Error()), nil, nil
2169+
}
2170+
2171+
// Optional parameters
2172+
body, err := OptionalParam[string](args, "body")
2173+
if err != nil {
2174+
return utils.NewToolResultError(err.Error()), nil, nil
2175+
}
2176+
2177+
// Get assignees
2178+
assignees, err := OptionalStringArrayParam(args, "assignees")
2179+
if err != nil {
2180+
return utils.NewToolResultError(err.Error()), nil, nil
2181+
}
2182+
2183+
// Get labels
2184+
labels, err := OptionalStringArrayParam(args, "labels")
2185+
if err != nil {
2186+
return utils.NewToolResultError(err.Error()), nil, nil
2187+
}
2188+
2189+
// Get optional milestone
2190+
milestone, err := OptionalIntParam(args, "milestone")
2191+
if err != nil {
2192+
return utils.NewToolResultError(err.Error()), nil, nil
2193+
}
2194+
2195+
var milestoneNum int
2196+
if milestone != 0 {
2197+
milestoneNum = milestone
2198+
}
2199+
2200+
// Get optional type
2201+
issueType, err := OptionalParam[string](args, "type")
2202+
if err != nil {
2203+
return utils.NewToolResultError(err.Error()), nil, nil
2204+
}
2205+
2206+
// Handle state, state_reason and duplicateOf parameters
2207+
state, err := OptionalParam[string](args, "state")
2208+
if err != nil {
2209+
return utils.NewToolResultError(err.Error()), nil, nil
2210+
}
2211+
2212+
stateReason, err := OptionalParam[string](args, "state_reason")
2213+
if err != nil {
2214+
return utils.NewToolResultError(err.Error()), nil, nil
2215+
}
2216+
2217+
duplicateOf, err := OptionalIntParam(args, "duplicate_of")
2218+
if err != nil {
2219+
return utils.NewToolResultError(err.Error()), nil, nil
2220+
}
2221+
if duplicateOf != 0 && stateReason != "duplicate" {
2222+
return utils.NewToolResultError("duplicate_of can only be used when state_reason is 'duplicate'"), nil, nil
2223+
}
2224+
2225+
client, err := deps.GetClient(ctx)
2226+
if err != nil {
2227+
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
2228+
}
2229+
2230+
gqlClient, err := deps.GetGQLClient(ctx)
2231+
if err != nil {
2232+
return utils.NewToolResultErrorFromErr("failed to get GraphQL client", err), nil, nil
2233+
}
2234+
2235+
switch method {
2236+
case "create":
2237+
result, err := CreateIssue(ctx, client, owner, repo, title, body, assignees, labels, milestoneNum, issueType, nil)
2238+
return result, nil, err
2239+
case "update":
2240+
issueNumber, err := RequiredInt(args, "issue_number")
2241+
if err != nil {
2242+
return utils.NewToolResultError(err.Error()), nil, nil
2243+
}
2244+
result, err := UpdateIssue(ctx, client, gqlClient, owner, repo, issueNumber, title, body, assignees, labels, milestoneNum, issueType, nil, nil, state, stateReason, duplicateOf)
2245+
return result, nil, err
2246+
default:
2247+
return utils.NewToolResultError("invalid method, must be either 'create' or 'update'"), nil, nil
2248+
}
2249+
})
2250+
st.FeatureFlagDisable = []string{FeatureFlagIssuesGranular, FeatureFlagIssueFields}
20532251
return st
20542252
}
20552253

0 commit comments

Comments
 (0)