Skip to content

Commit 5cd0dff

Browse files
owenniblockCopilot
andcommitted
Add multi-select issue field support, gated behind FF
Multi-select issue fields ride on the existing custom-fields surface: - issue_write (consolidated) and set_issue_fields (granular) gain multi-select inputs (field_option_names / multi_select_option_ids) - list_issues field_filters gain a 'values' slot for multi-select filtering with AND semantics - list_issue_fields advertises multi_select in its description and surfaces multi-select definitions - Read paths (IssueFieldValueFragment, list_issues enrichment, etc.) decode multi-select values when an org has them The write surface is gated behind a new FF remote_mcp_issue_fields_multiselect. When the flag is off, the legacy variants of issue_write, list_issues, and list_issue_fields are served — same handler bodies, but their schemas and descriptions omit multi_select. Read paths stay unchanged: orgs that have dotcom-side multi-select enabled continue to see multi-select VALUES surfaced, matching what the dotcom UI shows. The granular tools (set_issue_fields) are not separately gated — they are already behind FeatureFlagIssuesGranular, which is itself a user-opt-in rollout flag. Double-gating adds complexity without proportionate benefit for users who have already accepted experimental territory. Per the user's preference for code duplication over interleaved branching: - IssueWrite and IssueWriteLegacy are full siblings (issues.go + issues_legacy_multiselect.go). The shared parser and resolver (optionalIssueWriteFields, resolveIssueRequestFieldValues) take a single multiSelectEnabled bool and reject multi-select inputs/fields when false. - ListIssues / ListIssuesLegacy share a buildListIssues helper that swaps in the right descriptions and adds/removes the field_filters[] values slot. - ListIssueFields / ListIssueFieldsLegacy share a buildListIssueFields helper that swaps the description (handler is identical). Snapshot naming follows the established convention: legacy variants own the canonical <name>.snap; MS-aware variants own <name>_ff_remote_mcp_issue_fields_multiselect.snap. Stacked on #2755 (the universal delete-fix half of the original PR). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b399b6c commit 5cd0dff

17 files changed

Lines changed: 1924 additions & 236 deletions

docs/feature-flags.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ runtime behavior (such as output formatting) won't appear here.
131131

132132
- **set_issue_fields** - Set Issue Fields
133133
- **Required OAuth Scopes**: `repo`
134-
- `fields`: Array of issue field values to set. Each element must have a 'field_id' (string, the GraphQL node ID of the field) and exactly one value field: 'text_value' for text fields, 'number_value' for number fields, 'date_value' (ISO 8601 date string) for date fields, or 'single_select_option_id' (the GraphQL node ID of the option) for single select fields. Set 'delete' to true to remove a field value. (object[], required)
134+
- `fields`: Array of issue field values to set. Each element must have a 'field_id' (string, the GraphQL node ID of the field) and exactly one value field: 'text_value' for text fields, 'number_value' for number fields, 'date_value' (ISO 8601 date string) for date fields, 'single_select_option_id' (the GraphQL node ID of the option) for single select fields, or 'multi_select_option_ids' (an array of GraphQL node IDs) for multi select fields. Set 'delete' to true to remove a field value. (object[], required)
135135
- `issue_number`: The issue number to update (number, required)
136136
- `owner`: Repository owner (username or organization) (string, required)
137137
- `repo`: Repository name (string, required)
@@ -283,4 +283,40 @@ runtime behavior (such as output formatting) won't appear here.
283283
- `repo`: Repository name (string, required)
284284
- `start_line`: Optional 1-based starting line of the window of interest. Only ranges overlapping [start_line, end_line] are returned, clamped to the window. (number, optional)
285285

286+
### `remote_mcp_issue_fields_multiselect`
287+
288+
- **issue_write** - Create or update issue/pull request
289+
- **Required OAuth Scopes**: `repo`
290+
- `assignees`: Usernames to assign to this issue (string[], optional)
291+
- `body`: Issue body content (string, optional)
292+
- `duplicate_of`: Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'. (number, optional)
293+
- `issue_fields`: Issue field values to set or clear. Each item requires 'field_name' and exactly one of 'value', 'field_option_name', 'field_option_names', or 'delete: true'. (object[], optional)
294+
- `issue_number`: Issue number to update (number, optional)
295+
- `labels`: Labels to apply to this issue (string[], optional)
296+
- `method`: Write operation to perform on a single issue.
297+
Options are:
298+
- 'create' - creates a new issue.
299+
- 'update' - updates an existing issue.
300+
(string, required)
301+
- `milestone`: Milestone number (number, optional)
302+
- `owner`: Repository owner (string, required)
303+
- `repo`: Repository name (string, required)
304+
- `state`: New state (string, optional)
305+
- `state_reason`: Reason for the state change. Ignored unless state is changed. (string, optional)
306+
- `title`: Issue title (string, optional)
307+
- `type`: Type of this issue. Only use if issue types are enabled for this repository. Use list_issue_types tool to get valid type values for this repository or its owner organization. If the repository doesn't support issue types, omit this parameter. (string, optional)
308+
309+
- **list_issues** - List issues
310+
- **Required OAuth Scopes**: `repo`
311+
- `after`: Cursor for pagination. Use the cursor from the previous response. (string, optional)
312+
- `direction`: Order direction. If provided, the 'orderBy' also needs to be provided. (string, optional)
313+
- `field_filters`: Filter by custom issue field values. Each entry takes a field_name and either 'value' (text, number, YYYY-MM-DD date, or single-select option name) or 'values' (multi-select option names). For multi-select fields, all listed values must be set on an issue for it to match (AND semantics) — to match any-of, make multiple list_issues calls and union the results. (object[], optional)
314+
- `labels`: Filter by labels (string[], optional)
315+
- `orderBy`: Order issues by field. If provided, the 'direction' also needs to be provided. (string, optional)
316+
- `owner`: Repository owner (string, required)
317+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
318+
- `repo`: Repository name (string, required)
319+
- `since`: Filter by date (ISO 8601 timestamp) (string, optional)
320+
- `state`: Filter by state, by default both open and closed issues are returned when not provided (string, optional)
321+
286322
<!-- END AUTOMATED FEATURE FLAG TOOLS -->

docs/insiders-features.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,42 @@ The list below is generated from the Go source. It covers tool **inventory and s
103103
- `repo`: Repository name (string, required)
104104
- `start_line`: Optional 1-based starting line of the window of interest. Only ranges overlapping [start_line, end_line] are returned, clamped to the window. (number, optional)
105105

106+
### `remote_mcp_issue_fields_multiselect`
107+
108+
- **issue_write** - Create or update issue/pull request
109+
- **Required OAuth Scopes**: `repo`
110+
- `assignees`: Usernames to assign to this issue (string[], optional)
111+
- `body`: Issue body content (string, optional)
112+
- `duplicate_of`: Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'. (number, optional)
113+
- `issue_fields`: Issue field values to set or clear. Each item requires 'field_name' and exactly one of 'value', 'field_option_name', 'field_option_names', or 'delete: true'. (object[], optional)
114+
- `issue_number`: Issue number to update (number, optional)
115+
- `labels`: Labels to apply to this issue (string[], optional)
116+
- `method`: Write operation to perform on a single issue.
117+
Options are:
118+
- 'create' - creates a new issue.
119+
- 'update' - updates an existing issue.
120+
(string, required)
121+
- `milestone`: Milestone number (number, optional)
122+
- `owner`: Repository owner (string, required)
123+
- `repo`: Repository name (string, required)
124+
- `state`: New state (string, optional)
125+
- `state_reason`: Reason for the state change. Ignored unless state is changed. (string, optional)
126+
- `title`: Issue title (string, optional)
127+
- `type`: Type of this issue. Only use if issue types are enabled for this repository. Use list_issue_types tool to get valid type values for this repository or its owner organization. If the repository doesn't support issue types, omit this parameter. (string, optional)
128+
129+
- **list_issues** - List issues
130+
- **Required OAuth Scopes**: `repo`
131+
- `after`: Cursor for pagination. Use the cursor from the previous response. (string, optional)
132+
- `direction`: Order direction. If provided, the 'orderBy' also needs to be provided. (string, optional)
133+
- `field_filters`: Filter by custom issue field values. Each entry takes a field_name and either 'value' (text, number, YYYY-MM-DD date, or single-select option name) or 'values' (multi-select option names). For multi-select fields, all listed values must be set on an issue for it to match (AND semantics) — to match any-of, make multiple list_issues calls and union the results. (object[], optional)
134+
- `labels`: Filter by labels (string[], optional)
135+
- `orderBy`: Order issues by field. If provided, the 'direction' also needs to be provided. (string, optional)
136+
- `owner`: Repository owner (string, required)
137+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
138+
- `repo`: Repository name (string, required)
139+
- `since`: Filter by date (ISO 8601 timestamp) (string, optional)
140+
- `state`: Filter by state, by default both open and closed issues are returned when not provided (string, optional)
141+
106142
<!-- END AUTOMATED INSIDERS TOOLS -->
107143

108144
---
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
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/pull request"
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', 'field_option_names', 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', 'field_option_name', or 'field_option_names'.",
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', 'field_option_names', or 'delete'.",
50+
"type": "string"
51+
},
52+
"field_option_names": {
53+
"description": "Option names for multi-select fields. All names are validated against the field's options before the API call. An empty array is rejected — use 'delete: true' to clear the field. Cannot be combined with 'value', 'field_option_name', or 'delete'.",
54+
"items": {
55+
"type": "string"
56+
},
57+
"type": "array"
58+
},
59+
"value": {
60+
"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', 'field_option_names', or 'delete'.",
61+
"type": [
62+
"string",
63+
"number",
64+
"boolean"
65+
]
66+
}
67+
},
68+
"required": [
69+
"field_name"
70+
],
71+
"type": "object"
72+
},
73+
"type": "array"
74+
},
75+
"issue_number": {
76+
"description": "Issue number to update",
77+
"type": "number"
78+
},
79+
"labels": {
80+
"description": "Labels to apply to this issue",
81+
"items": {
82+
"type": "string"
83+
},
84+
"type": "array"
85+
},
86+
"method": {
87+
"description": "Write operation to perform on a single issue.\nOptions are:\n- 'create' - creates a new issue.\n- 'update' - updates an existing issue.\n",
88+
"enum": [
89+
"create",
90+
"update"
91+
],
92+
"type": "string"
93+
},
94+
"milestone": {
95+
"description": "Milestone number",
96+
"type": "number"
97+
},
98+
"owner": {
99+
"description": "Repository owner",
100+
"type": "string"
101+
},
102+
"repo": {
103+
"description": "Repository name",
104+
"type": "string"
105+
},
106+
"show_ui": {
107+
"description": "Whether to render the MCP App form instead of executing the request immediately. Defaults to true. Set to false to skip the form and execute directly — useful when you have all required values (especially ones the form does not collect, like labels, assignees, milestone, type, issue_fields, or state changes) and the user has already confirmed the action.",
108+
"type": "boolean"
109+
},
110+
"state": {
111+
"description": "New state",
112+
"enum": [
113+
"open",
114+
"closed"
115+
],
116+
"type": "string"
117+
},
118+
"state_reason": {
119+
"description": "Reason for the state change. Ignored unless state is changed.",
120+
"enum": [
121+
"completed",
122+
"not_planned",
123+
"duplicate"
124+
],
125+
"type": "string"
126+
},
127+
"title": {
128+
"description": "Issue title",
129+
"type": "string"
130+
},
131+
"type": {
132+
"description": "Type of this issue. Only use if issue types are enabled for this repository. Use list_issue_types tool to get valid type values for this repository or its owner organization. If the repository doesn't support issue types, omit this parameter.",
133+
"type": "string"
134+
}
135+
},
136+
"required": [
137+
"method",
138+
"owner",
139+
"repo"
140+
],
141+
"type": "object"
142+
},
143+
"name": "issue_write"
144+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"annotations": {
3+
"readOnlyHint": true,
4+
"title": "List issue fields"
5+
},
6+
"description": "List issue fields for a repository or organization. Returns field definitions including name, type (text, number, date, single_select, multi_select), and for single_select and multi_select fields the list of valid option names. When repo is omitted, returns org-level fields directly.",
7+
"inputSchema": {
8+
"properties": {
9+
"owner": {
10+
"description": "The account owner of the repository or organization. The name is not case sensitive.",
11+
"type": "string"
12+
},
13+
"repo": {
14+
"description": "The name of the repository. When provided, returns fields for this specific repository (inherited from its organization). When omitted, returns org-level fields directly.",
15+
"type": "string"
16+
}
17+
},
18+
"required": [
19+
"owner"
20+
],
21+
"type": "object"
22+
},
23+
"name": "list_issue_fields"
24+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
{
2+
"annotations": {
3+
"readOnlyHint": true,
4+
"title": "List issues"
5+
},
6+
"description": "List issues in a GitHub repository. For pagination, use the 'endCursor' from the previous response's 'pageInfo' in the 'after' parameter.",
7+
"inputSchema": {
8+
"properties": {
9+
"after": {
10+
"description": "Cursor for pagination. Use the cursor from the previous response.",
11+
"type": "string"
12+
},
13+
"direction": {
14+
"description": "Order direction. If provided, the 'orderBy' also needs to be provided.",
15+
"enum": [
16+
"ASC",
17+
"DESC"
18+
],
19+
"type": "string"
20+
},
21+
"field_filters": {
22+
"description": "Filter by custom issue field values. Each entry takes a field_name and either 'value' (text, number, YYYY-MM-DD date, or single-select option name) or 'values' (multi-select option names). For multi-select fields, all listed values must be set on an issue for it to match (AND semantics) — to match any-of, make multiple list_issues calls and union the results.",
23+
"items": {
24+
"properties": {
25+
"field_name": {
26+
"description": "Name of the custom field (e.g. \"Priority\"). Case-insensitive.",
27+
"type": "string"
28+
},
29+
"value": {
30+
"description": "Value to filter on for text, number, date, or single-select fields. For single-select, the option name (e.g. \"P1\"). For dates, YYYY-MM-DD. For numbers, the numeric value as a string. For text, the text value. Cannot be combined with 'values'.",
31+
"type": "string"
32+
},
33+
"values": {
34+
"description": "Option names to filter on for multi-select fields. Matches issues that have ALL of these options set (AND semantics). To match any-of, make multiple list_issues calls. Cannot be combined with 'value'.",
35+
"items": {
36+
"type": "string"
37+
},
38+
"type": "array"
39+
}
40+
},
41+
"required": [
42+
"field_name"
43+
],
44+
"type": "object"
45+
},
46+
"type": "array"
47+
},
48+
"labels": {
49+
"description": "Filter by labels",
50+
"items": {
51+
"type": "string"
52+
},
53+
"type": "array"
54+
},
55+
"orderBy": {
56+
"description": "Order issues by field. If provided, the 'direction' also needs to be provided.",
57+
"enum": [
58+
"CREATED_AT",
59+
"UPDATED_AT",
60+
"COMMENTS"
61+
],
62+
"type": "string"
63+
},
64+
"owner": {
65+
"description": "Repository owner",
66+
"type": "string"
67+
},
68+
"perPage": {
69+
"description": "Results per page for pagination (min 1, max 100)",
70+
"maximum": 100,
71+
"minimum": 1,
72+
"type": "number"
73+
},
74+
"repo": {
75+
"description": "Repository name",
76+
"type": "string"
77+
},
78+
"since": {
79+
"description": "Filter by date (ISO 8601 timestamp)",
80+
"type": "string"
81+
},
82+
"state": {
83+
"description": "Filter by state, by default both open and closed issues are returned when not provided",
84+
"enum": [
85+
"OPEN",
86+
"CLOSED"
87+
],
88+
"type": "string"
89+
}
90+
},
91+
"required": [
92+
"owner",
93+
"repo"
94+
],
95+
"type": "object"
96+
},
97+
"name": "list_issues"
98+
}

pkg/github/__toolsnaps__/set_issue_fields.snap

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"inputSchema": {
99
"properties": {
1010
"fields": {
11-
"description": "Array of issue field values to set. Each element must have a 'field_id' (string, the GraphQL node ID of the field) and exactly one value field: 'text_value' for text fields, 'number_value' for number fields, 'date_value' (ISO 8601 date string) for date fields, or 'single_select_option_id' (the GraphQL node ID of the option) for single select fields. Set 'delete' to true to remove a field value.",
11+
"description": "Array of issue field values to set. Each element must have a 'field_id' (string, the GraphQL node ID of the field) and exactly one value field: 'text_value' for text fields, 'number_value' for number fields, 'date_value' (ISO 8601 date string) for date fields, 'single_select_option_id' (the GraphQL node ID of the option) for single select fields, or 'multi_select_option_ids' (an array of GraphQL node IDs) for multi select fields. Set 'delete' to true to remove a field value.",
1212
"items": {
1313
"properties": {
1414
"confidence": {
@@ -36,6 +36,13 @@
3636
"description": "If true, this field value is sent to the API as a suggestion (suggest:true) rather than an applied value. Whether the value is applied or recorded as a proposal is determined by the API.",
3737
"type": "boolean"
3838
},
39+
"multi_select_option_ids": {
40+
"description": "The GraphQL node IDs of the options to set for a multi select field",
41+
"items": {
42+
"type": "string"
43+
},
44+
"type": "array"
45+
},
3946
"number_value": {
4047
"description": "The value to set for a number field",
4148
"type": "number"

0 commit comments

Comments
 (0)