Skip to content

Commit ccad8f8

Browse files
authored
feat: gate issue_write and get_issue behind remote_mcp_issue_fields flag
- Extend FeatureFlagEnable/Disable fields from string to []string (AND semantics for enable, OR semantics for disable) so a tool can require multiple flags simultaneously. - Add LegacyIssueWrite: 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. Both this and IssueWrite register under the tool name 'issue_write'; exactly one is active at a time via mutually exclusive flag annotations. - Gate IssueWrite (the flag-enabled variant) with FeatureFlagEnable so it is only served when remote_mcp_issue_fields is on. - Gate the get_issue field_values enrichment with a runtime IsFeatureEnabled check so the GraphQL round-trip is skipped when the flag is off. - Add issue_write.snap (legacy) and issue_write_ff_remote_mcp_issue_fields.snap (flag-enabled) toolsnaps following the convention established by PR #2520. - Modernise featureFlagAllowed disableFlags loop to slices.ContainsFunc.
1 parent 15192ca commit ccad8f8

21 files changed

Lines changed: 508 additions & 129 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. Each item requires 'field_name' and exactly one of 'value' or 'field_option_name'. (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. Each item requires 'field_name' and exactly one of 'value' or 'field_option_name'. (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. Each item requires 'field_name' and exactly one of 'value' or 'field_option_name'. (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. Each item requires 'field_name' and exactly one of 'value' or 'field_option_name'. (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. Each item requires 'field_name' and exactly one of 'value' or 'field_option_name'. (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 & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -29,57 +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. Each item requires 'field_name' and exactly one of 'value' or 'field_option_name'.",
34-
"items": {
35-
"additionalProperties": false,
36-
"oneOf": [
37-
{
38-
"not": {
39-
"required": [
40-
"field_option_name"
41-
]
42-
},
43-
"required": [
44-
"value"
45-
]
46-
},
47-
{
48-
"not": {
49-
"required": [
50-
"value"
51-
]
52-
},
53-
"required": [
54-
"field_option_name"
55-
]
56-
}
57-
],
58-
"properties": {
59-
"field_name": {
60-
"description": "Issue field name",
61-
"type": "string"
62-
},
63-
"field_option_name": {
64-
"description": "Option name for single-select fields — validates the option exists in the field definition before setting it.",
65-
"type": "string"
66-
},
67-
"value": {
68-
"description": "Value to set. For single-select fields, prefer 'field_option_name' to validate the option exists first.",
69-
"type": [
70-
"string",
71-
"number",
72-
"boolean"
73-
]
74-
}
75-
},
76-
"required": [
77-
"field_name"
78-
],
79-
"type": "object"
80-
},
81-
"type": "array"
82-
},
8332
"issue_number": {
8433
"description": "Issue number to update",
8534
"type": "number"
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
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. Each item requires 'field_name' and exactly one of 'value' or 'field_option_name'.",
34+
"items": {
35+
"additionalProperties": false,
36+
"oneOf": [
37+
{
38+
"not": {
39+
"required": [
40+
"field_option_name"
41+
]
42+
},
43+
"required": [
44+
"value"
45+
]
46+
},
47+
{
48+
"not": {
49+
"required": [
50+
"value"
51+
]
52+
},
53+
"required": [
54+
"field_option_name"
55+
]
56+
}
57+
],
58+
"properties": {
59+
"field_name": {
60+
"description": "Issue field name",
61+
"type": "string"
62+
},
63+
"field_option_name": {
64+
"description": "Option name for single-select fields — validates the option exists in the field definition before setting it.",
65+
"type": "string"
66+
},
67+
"value": {
68+
"description": "Value to set. For single-select fields, prefer 'field_option_name' to validate the option exists first.",
69+
"type": [
70+
"string",
71+
"number",
72+
"boolean"
73+
]
74+
}
75+
},
76+
"required": [
77+
"field_name"
78+
],
79+
"type": "object"
80+
},
81+
"type": "array"
82+
},
83+
"issue_number": {
84+
"description": "Issue number to update",
85+
"type": "number"
86+
},
87+
"labels": {
88+
"description": "Labels to apply to this issue",
89+
"items": {
90+
"type": "string"
91+
},
92+
"type": "array"
93+
},
94+
"method": {
95+
"description": "Write operation to perform on a single issue.\nOptions are:\n- 'create' - creates a new issue.\n- 'update' - updates an existing issue.\n",
96+
"enum": [
97+
"create",
98+
"update"
99+
],
100+
"type": "string"
101+
},
102+
"milestone": {
103+
"description": "Milestone number",
104+
"type": "number"
105+
},
106+
"owner": {
107+
"description": "Repository owner",
108+
"type": "string"
109+
},
110+
"repo": {
111+
"description": "Repository name",
112+
"type": "string"
113+
},
114+
"state": {
115+
"description": "New state",
116+
"enum": [
117+
"open",
118+
"closed"
119+
],
120+
"type": "string"
121+
},
122+
"state_reason": {
123+
"description": "Reason for the state change. Ignored unless state is changed.",
124+
"enum": [
125+
"completed",
126+
"not_planned",
127+
"duplicate"
128+
],
129+
"type": "string"
130+
},
131+
"title": {
132+
"description": "Issue title",
133+
"type": "string"
134+
},
135+
"type": {
136+
"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.",
137+
"type": "string"
138+
}
139+
},
140+
"required": [
141+
"method",
142+
"owner",
143+
"repo"
144+
],
145+
"type": "object"
146+
},
147+
"name": "issue_write"
148+
}

pkg/github/csv_output_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ func TestCSVOutputAppliedToDefaultListTools(t *testing.T) {
4040

4141
func TestCSVOutputAppliesToFlagGatedListTools(t *testing.T) {
4242
enabledOnly := testCSVOutputTool("list_things", `[{"number":1}]`)
43-
enabledOnly.FeatureFlagEnable = FeatureFlagIssueFields
43+
enabledOnly.FeatureFlagEnable = []string{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/granular_tools_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020
func granularToolsForToolset(toolsetID inventory.ToolsetID, featureFlag string) []inventory.ServerTool {
2121
var result []inventory.ServerTool
2222
for _, tool := range AllTools(translations.NullTranslationHelper) {
23-
if tool.Toolset.ID == toolsetID && tool.FeatureFlagEnable == featureFlag {
23+
if tool.Toolset.ID == toolsetID && len(tool.FeatureFlagEnable) > 0 && tool.FeatureFlagEnable[0] == featureFlag {
2424
result = append(result, tool)
2525
}
2626
}
@@ -94,7 +94,7 @@ func TestIssuesGranularToolset(t *testing.T) {
9494

9595
t.Run("all granular tools have correct feature flag", func(t *testing.T) {
9696
for _, tool := range granularToolsForToolset(ToolsetMetadataIssues.ID, FeatureFlagIssuesGranular) {
97-
assert.Equal(t, FeatureFlagIssuesGranular, tool.FeatureFlagEnable, "tool %s", tool.Tool.Name)
97+
assert.Equal(t, []string{FeatureFlagIssuesGranular}, tool.FeatureFlagEnable, "tool %s", tool.Tool.Name)
9898
}
9999
})
100100
}
@@ -129,7 +129,7 @@ func TestPullRequestsGranularToolset(t *testing.T) {
129129

130130
t.Run("all granular tools have correct feature flag", func(t *testing.T) {
131131
for _, tool := range granularToolsForToolset(ToolsetMetadataPullRequests.ID, FeatureFlagPullRequestsGranular) {
132-
assert.Equal(t, FeatureFlagPullRequestsGranular, tool.FeatureFlagEnable, "tool %s", tool.Tool.Name)
132+
assert.Equal(t, []string{FeatureFlagPullRequestsGranular}, tool.FeatureFlagEnable, "tool %s", tool.Tool.Name)
133133
}
134134
})
135135
}

pkg/github/issue_fields.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ func ListIssueFields(t translations.TranslationHelperFunc) inventory.ServerTool
157157

158158
return utils.NewToolResultText(string(r)), nil, nil
159159
})
160-
st.FeatureFlagEnable = FeatureFlagIssueFields
160+
st.FeatureFlagEnable = []string{FeatureFlagIssueFields}
161161
return st
162162
}
163163

0 commit comments

Comments
 (0)