Skip to content

Commit eacc281

Browse files
feat: gate issue_write and get_issue behind remote_mcp_issue_fields flag
Ports the gating from PR #2553 onto main (the original merge landed on a stack base that did not make it to main). Changes: - pkg/inventory: FeatureFlagDisable becomes []string (any-listed-on → hide). FeatureFlagEnable stays as a single string. This avoids the AND-of-enable semantics from the earlier proposal, which encoded dependencies rather than rollout knobs and had no real call site. Disable-OR is the case that does need the slice (LegacyIssueWrite below). - pkg/github/issues.go: split IssueWrite into IssueWrite (flag-enabled, exposes issue_fields) and LegacyIssueWrite (flag-disabled, omits it). Both register as 'issue_write'; mutually exclusive flag annotations pick exactly one at runtime. Refactored into a shared buildIssueWrite helper instead of duplicating the ~250-line tool definition. - pkg/github/issues.go: GetIssue field_values enrichment now requires the flag at runtime. The verbose REST IssueFieldValues is always cleared from the response. - Existing single-flag Disable call sites converted to slices. - New toolsnap variant issue_write_ff_remote_mcp_issue_fields.snap; the canonical issue_write.snap is owned by LegacyIssueWrite. - README + flag docs regenerated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e091ea6 commit eacc281

17 files changed

Lines changed: 286 additions & 105 deletions

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -855,7 +855,6 @@ The following sets of tools are available:
855855
- `assignees`: Usernames to assign to this issue (string[], optional)
856856
- `body`: Issue body content (string, optional)
857857
- `duplicate_of`: Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'. (number, optional)
858-
- `issue_fields`: Issue field values to set or clear. Each item requires 'field_name' and exactly one of 'value', 'field_option_name', or 'delete: true'. (object[], optional)
859858
- `issue_number`: Issue number to update (number, optional)
860859
- `labels`: Labels to apply to this issue (string[], optional)
861860
- `method`: Write operation to perform on a single issue.

docs/feature-flags.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ runtime behavior (such as output formatting) won't appear here.
5656
- `assignees`: Usernames to assign to this issue (string[], optional)
5757
- `body`: Issue body content (string, optional)
5858
- `duplicate_of`: Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'. (number, optional)
59-
- `issue_fields`: Issue field values to set or clear. Each item requires 'field_name' and exactly one of 'value', 'field_option_name', or 'delete: true'. (object[], optional)
6059
- `issue_number`: Issue number to update (number, optional)
6160
- `labels`: Labels to apply to this issue (string[], optional)
6261
- `method`: Write operation to perform on a single issue.
@@ -74,6 +73,27 @@ runtime behavior (such as output formatting) won't appear here.
7473

7574
### `remote_mcp_issue_fields`
7675

76+
- **issue_write** - Create or update issue
77+
- **Required OAuth Scopes**: `repo`
78+
- `assignees`: Usernames to assign to this issue (string[], optional)
79+
- `body`: Issue body content (string, optional)
80+
- `duplicate_of`: Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'. (number, optional)
81+
- `issue_fields`: Issue field values to set or clear. Each item requires 'field_name' and exactly one of 'value', 'field_option_name', or 'delete: true'. (object[], optional)
82+
- `issue_number`: Issue number to update (number, optional)
83+
- `labels`: Labels to apply to this issue (string[], optional)
84+
- `method`: Write operation to perform on a single issue.
85+
Options are:
86+
- 'create' - creates a new issue.
87+
- 'update' - updates an existing issue.
88+
(string, required)
89+
- `milestone`: Milestone number (number, optional)
90+
- `owner`: Repository owner (string, required)
91+
- `repo`: Repository name (string, required)
92+
- `state`: New state (string, optional)
93+
- `state_reason`: Reason for the state change. Ignored unless state is changed. (string, optional)
94+
- `title`: Issue title (string, optional)
95+
- `type`: 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. (string, optional)
96+
7797
- **list_issue_fields** - List issue fields
7898
- **Required OAuth Scopes**: `repo`, `read:org`
7999
- **Accepted OAuth Scopes**: `admin:org`, `read:org`, `repo`, `write:org`

docs/insiders-features.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ The list below is generated from the Go source. It covers tool **inventory and s
5050
- `assignees`: Usernames to assign to this issue (string[], optional)
5151
- `body`: Issue body content (string, optional)
5252
- `duplicate_of`: Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'. (number, optional)
53-
- `issue_fields`: Issue field values to set or clear. Each item requires 'field_name' and exactly one of 'value', 'field_option_name', or 'delete: true'. (object[], optional)
5453
- `issue_number`: Issue number to update (number, optional)
5554
- `labels`: Labels to apply to this issue (string[], optional)
5655
- `method`: Write operation to perform on a single issue.
@@ -68,6 +67,27 @@ The list below is generated from the Go source. It covers tool **inventory and s
6867

6968
### `remote_mcp_issue_fields`
7069

70+
- **issue_write** - Create or update issue
71+
- **Required OAuth Scopes**: `repo`
72+
- `assignees`: Usernames to assign to this issue (string[], optional)
73+
- `body`: Issue body content (string, optional)
74+
- `duplicate_of`: Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'. (number, optional)
75+
- `issue_fields`: Issue field values to set or clear. Each item requires 'field_name' and exactly one of 'value', 'field_option_name', or 'delete: true'. (object[], optional)
76+
- `issue_number`: Issue number to update (number, optional)
77+
- `labels`: Labels to apply to this issue (string[], optional)
78+
- `method`: Write operation to perform on a single issue.
79+
Options are:
80+
- 'create' - creates a new issue.
81+
- 'update' - updates an existing issue.
82+
(string, required)
83+
- `milestone`: Milestone number (number, optional)
84+
- `owner`: Repository owner (string, required)
85+
- `repo`: Repository name (string, required)
86+
- `state`: New state (string, optional)
87+
- `state_reason`: Reason for the state change. Ignored unless state is changed. (string, optional)
88+
- `title`: Issue title (string, optional)
89+
- `type`: 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. (string, optional)
90+
7191
- **list_issue_fields** - List issue fields
7292
- **Required OAuth Scopes**: `repo`, `read:org`
7393
- **Accepted OAuth Scopes**: `admin:org`, `read:org`, `repo`, `write:org`

pkg/github/__toolsnaps__/issue_write.snap

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -29,42 +29,6 @@
2929
"description": "Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'.",
3030
"type": "number"
3131
},
32-
"issue_fields": {
33-
"description": "Issue field values to set or clear. Each item requires 'field_name' and exactly one of 'value', 'field_option_name', or 'delete: true'.",
34-
"items": {
35-
"additionalProperties": false,
36-
"properties": {
37-
"delete": {
38-
"description": "Set to true to clear this field's current value on the issue. Cannot be combined with 'value' or 'field_option_name'.",
39-
"enum": [
40-
true
41-
],
42-
"type": "boolean"
43-
},
44-
"field_name": {
45-
"description": "Issue field name (case-insensitive). Must match a field returned by list_issue_fields for this repository or its organization.",
46-
"type": "string"
47-
},
48-
"field_option_name": {
49-
"description": "Option name for single-select fields. Validated against the field's options before the API call. Cannot be combined with 'value' or 'delete'.",
50-
"type": "string"
51-
},
52-
"value": {
53-
"description": "Value to set. Use for text, number, and date fields (date as YYYY-MM-DD). For single-select fields, prefer 'field_option_name' so the option is validated before the API call. Cannot be combined with 'field_option_name' or 'delete'.",
54-
"type": [
55-
"string",
56-
"number",
57-
"boolean"
58-
]
59-
}
60-
},
61-
"required": [
62-
"field_name"
63-
],
64-
"type": "object"
65-
},
66-
"type": "array"
67-
},
6832
"issue_number": {
6933
"description": "Issue number to update",
7034
"type": "number"
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
{
2+
"_meta": {
3+
"ui": {
4+
"resourceUri": "ui://github-mcp-server/issue-write",
5+
"visibility": [
6+
"model",
7+
"app"
8+
]
9+
}
10+
},
11+
"annotations": {
12+
"title": "Create or update issue"
13+
},
14+
"description": "Create a new or update an existing issue in a GitHub repository.",
15+
"inputSchema": {
16+
"properties": {
17+
"assignees": {
18+
"description": "Usernames to assign to this issue",
19+
"items": {
20+
"type": "string"
21+
},
22+
"type": "array"
23+
},
24+
"body": {
25+
"description": "Issue body content",
26+
"type": "string"
27+
},
28+
"duplicate_of": {
29+
"description": "Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'.",
30+
"type": "number"
31+
},
32+
"issue_fields": {
33+
"description": "Issue field values to set or clear. Each item requires 'field_name' and exactly one of 'value', 'field_option_name', or 'delete: true'.",
34+
"items": {
35+
"additionalProperties": false,
36+
"properties": {
37+
"delete": {
38+
"description": "Set to true to clear this field's current value on the issue. Cannot be combined with 'value' or 'field_option_name'.",
39+
"enum": [
40+
true
41+
],
42+
"type": "boolean"
43+
},
44+
"field_name": {
45+
"description": "Issue field name (case-insensitive). Must match a field returned by list_issue_fields for this repository or its organization.",
46+
"type": "string"
47+
},
48+
"field_option_name": {
49+
"description": "Option name for single-select fields. Validated against the field's options before the API call. Cannot be combined with 'value' or 'delete'.",
50+
"type": "string"
51+
},
52+
"value": {
53+
"description": "Value to set. Use for text, number, and date fields (date as YYYY-MM-DD). For single-select fields, prefer 'field_option_name' so the option is validated before the API call. Cannot be combined with 'field_option_name' or 'delete'.",
54+
"type": [
55+
"string",
56+
"number",
57+
"boolean"
58+
]
59+
}
60+
},
61+
"required": [
62+
"field_name"
63+
],
64+
"type": "object"
65+
},
66+
"type": "array"
67+
},
68+
"issue_number": {
69+
"description": "Issue number to update",
70+
"type": "number"
71+
},
72+
"labels": {
73+
"description": "Labels to apply to this issue",
74+
"items": {
75+
"type": "string"
76+
},
77+
"type": "array"
78+
},
79+
"method": {
80+
"description": "Write operation to perform on a single issue.\nOptions are:\n- 'create' - creates a new issue.\n- 'update' - updates an existing issue.\n",
81+
"enum": [
82+
"create",
83+
"update"
84+
],
85+
"type": "string"
86+
},
87+
"milestone": {
88+
"description": "Milestone number",
89+
"type": "number"
90+
},
91+
"owner": {
92+
"description": "Repository owner",
93+
"type": "string"
94+
},
95+
"repo": {
96+
"description": "Repository name",
97+
"type": "string"
98+
},
99+
"state": {
100+
"description": "New state",
101+
"enum": [
102+
"open",
103+
"closed"
104+
],
105+
"type": "string"
106+
},
107+
"state_reason": {
108+
"description": "Reason for the state change. Ignored unless state is changed.",
109+
"enum": [
110+
"completed",
111+
"not_planned",
112+
"duplicate"
113+
],
114+
"type": "string"
115+
},
116+
"title": {
117+
"description": "Issue title",
118+
"type": "string"
119+
},
120+
"type": {
121+
"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.",
122+
"type": "string"
123+
}
124+
},
125+
"required": [
126+
"method",
127+
"owner",
128+
"repo"
129+
],
130+
"type": "object"
131+
},
132+
"name": "issue_write"
133+
}

pkg/github/csv_output_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func TestCSVOutputAppliesToFlagGatedListTools(t *testing.T) {
4242
enabledOnly := testCSVOutputTool("list_things", `[{"number":1}]`)
4343
enabledOnly.FeatureFlagEnable = FeatureFlagIssueFields
4444
disabledOnly := testCSVOutputTool("list_legacy_things", `[{"number":2}]`)
45-
disabledOnly.FeatureFlagDisable = FeatureFlagIssueFields
45+
disabledOnly.FeatureFlagDisable = []string{FeatureFlagIssueFields}
4646

4747
tools := withCSVOutput([]inventory.ServerTool{enabledOnly, disabledOnly})
4848
require.Len(t, tools, 2)

pkg/github/issues.go

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -884,13 +884,16 @@ func GetIssue(ctx context.Context, client *github.Client, deps ToolDependencies,
884884

885885
minimalIssue := convertToMinimalIssue(issue)
886886

887-
// Enrich with field_values via GraphQL for consistency with list_issues/search_issues
888-
if issue != nil && issue.NodeID != nil && *issue.NodeID != "" {
889-
gqlClient, err := deps.GetGQLClient(ctx)
890-
if err == nil {
891-
if fieldValuesByID, err := fetchIssueFieldValuesByNodeID(ctx, gqlClient, []*github.Issue{issue}); err == nil {
892-
minimalIssue.FieldValues = fieldValuesByID[*issue.NodeID]
893-
minimalIssue.IssueFieldValues = nil // Clear verbose REST format
887+
// Always drop the verbose REST IssueFieldValues; only enrich with the GraphQL
888+
// field_values view when the issue-fields feature flag is on.
889+
minimalIssue.IssueFieldValues = nil
890+
if deps.IsFeatureEnabled(ctx, FeatureFlagIssueFields) {
891+
if issue != nil && issue.NodeID != nil && *issue.NodeID != "" {
892+
gqlClient, err := deps.GetGQLClient(ctx)
893+
if err == nil {
894+
if fieldValuesByID, err := fetchIssueFieldValuesByNodeID(ctx, gqlClient, []*github.Issue{issue}); err == nil {
895+
minimalIssue.FieldValues = fieldValuesByID[*issue.NodeID]
896+
}
894897
}
895898
}
896899
}
@@ -1331,7 +1334,7 @@ Options are:
13311334
return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
13321335
}
13331336
})
1334-
st.FeatureFlagDisable = FeatureFlagIssuesGranular
1337+
st.FeatureFlagDisable = []string{FeatureFlagIssuesGranular}
13351338
return st
13361339
}
13371340

@@ -1754,11 +1757,33 @@ func searchIssuesHandler(ctx context.Context, deps ToolDependencies, args map[st
17541757
return callResult, nil
17551758
}
17561759

1757-
// IssueWrite creates a tool to create a new or update an existing issue in a GitHub repository.
17581760
// IssueWriteUIResourceURI is the URI for the issue_write tool's MCP App UI resource.
17591761
const IssueWriteUIResourceURI = "ui://github-mcp-server/issue-write"
17601762

1763+
// IssueWrite is the FeatureFlagIssueFields-enabled variant of issue_write
1764+
// (with the issue_fields parameter). LegacyIssueWrite is served when the flag
1765+
// 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.
17611769
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 {
17621787
st := NewTool(
17631788
ToolsetMetadataIssues,
17641789
mcp.Tool{
@@ -1978,9 +2003,12 @@ Options are:
19782003
return utils.NewToolResultError("duplicate_of can only be used when state_reason is 'duplicate'"), nil, nil
19792004
}
19802005

1981-
issueFields, err := optionalIssueWriteFields(args)
1982-
if err != nil {
1983-
return utils.NewToolResultError(err.Error()), nil, nil
2006+
var issueFields []issueWriteFieldInput
2007+
if includeIssueFields {
2008+
issueFields, err = optionalIssueWriteFields(args)
2009+
if err != nil {
2010+
return utils.NewToolResultError(err.Error()), nil, nil
2011+
}
19842012
}
19852013

19862014
client, err := deps.GetClient(ctx)
@@ -1993,9 +2021,13 @@ Options are:
19932021
return utils.NewToolResultErrorFromErr("failed to get GraphQL client", err), nil, nil
19942022
}
19952023

1996-
issueFieldValues, fieldIDsToDelete, err := resolveIssueRequestFieldValues(ctx, gqlClient, owner, repo, issueFields)
1997-
if err != nil {
1998-
return utils.NewToolResultError(fmt.Sprintf("failed to resolve issue_fields: %v", err)), nil, nil
2024+
var issueFieldValues []*github.IssueRequestFieldValue
2025+
var fieldIDsToDelete []int64
2026+
if len(issueFields) > 0 {
2027+
issueFieldValues, fieldIDsToDelete, err = resolveIssueRequestFieldValues(ctx, gqlClient, owner, repo, issueFields)
2028+
if err != nil {
2029+
return utils.NewToolResultError(fmt.Sprintf("failed to resolve issue_fields: %v", err)), nil, nil
2030+
}
19992031
}
20002032

20012033
switch method {
@@ -2013,7 +2045,11 @@ Options are:
20132045
return utils.NewToolResultError("invalid method, must be either 'create' or 'update'"), nil, nil
20142046
}
20152047
})
2016-
st.FeatureFlagDisable = FeatureFlagIssuesGranular
2048+
if !includeIssueFields {
2049+
if schema, ok := st.Tool.InputSchema.(*jsonschema.Schema); ok {
2050+
delete(schema.Properties, "issue_fields")
2051+
}
2052+
}
20172053
return st
20182054
}
20192055

@@ -2690,7 +2726,7 @@ func LegacyListIssues(t translations.TranslationHelperFunc) inventory.ServerTool
26902726
}
26912727
return result, nil, nil
26922728
})
2693-
st.FeatureFlagDisable = FeatureFlagIssueFields
2729+
st.FeatureFlagDisable = []string{FeatureFlagIssueFields}
26942730
return st
26952731
}
26962732

0 commit comments

Comments
 (0)