Skip to content

Commit 352a2bc

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 9bdbcaa commit 352a2bc

17 files changed

Lines changed: 1914 additions & 228 deletions

docs/feature-flags.md

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

144144
- **set_issue_fields** - Set Issue Fields
145145
- **Required OAuth Scopes**: `repo`
146-
- `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)
146+
- `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)
147147
- `issue_number`: The issue number to update (number, required)
148148
- `owner`: Repository owner (username or organization) (string, required)
149149
- `repo`: Repository name (string, required)
@@ -334,4 +334,40 @@ runtime behavior (such as output formatting) won't appear here.
334334
- 'blocked_by' - the subject issue is blocked by the related issue.
335335
- 'blocking' - the subject issue blocks the related issue. (string, required)
336336

337+
### `remote_mcp_issue_fields_multiselect`
338+
339+
- **issue_write** - Create or update issue/pull request
340+
- **Required OAuth Scopes**: `repo`
341+
- `assignees`: Usernames to assign to this issue (string[], optional)
342+
- `body`: Issue body content (string, optional)
343+
- `duplicate_of`: Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'. (number, optional)
344+
- `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)
345+
- `issue_number`: Issue number to update (number, optional)
346+
- `labels`: Labels to apply to this issue (string[], optional)
347+
- `method`: Write operation to perform on a single issue.
348+
Options are:
349+
- 'create' - creates a new issue.
350+
- 'update' - updates an existing issue.
351+
(string, required)
352+
- `milestone`: Milestone number (number, optional)
353+
- `owner`: Repository owner (string, required)
354+
- `repo`: Repository name (string, required)
355+
- `state`: New state (string, optional)
356+
- `state_reason`: Reason for the state change. Ignored unless state is changed. (string, optional)
357+
- `title`: Issue title (string, optional)
358+
- `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)
359+
360+
- **list_issues** - List issues
361+
- **Required OAuth Scopes**: `repo`
362+
- `after`: Cursor for pagination. Use the cursor from the previous response. (string, optional)
363+
- `direction`: Order direction. If provided, the 'orderBy' also needs to be provided. (string, optional)
364+
- `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)
365+
- `labels`: Filter by labels (string[], optional)
366+
- `orderBy`: Order issues by field. If provided, the 'direction' also needs to be provided. (string, optional)
367+
- `owner`: Repository owner (string, required)
368+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
369+
- `repo`: Repository name (string, required)
370+
- `since`: Filter by date (ISO 8601 timestamp) (string, optional)
371+
- `state`: Filter by state, by default both open and closed issues are returned when not provided (string, optional)
372+
337373
<!-- END AUTOMATED FEATURE FLAG TOOLS -->

docs/insiders-features.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,42 @@ The list below is generated from the Go source. It covers tool **inventory and s
133133
- 'blocked_by' - the subject issue is blocked by the related issue.
134134
- 'blocking' - the subject issue blocks the related issue. (string, required)
135135

136+
### `remote_mcp_issue_fields_multiselect`
137+
138+
- **issue_write** - Create or update issue/pull request
139+
- **Required OAuth Scopes**: `repo`
140+
- `assignees`: Usernames to assign to this issue (string[], optional)
141+
- `body`: Issue body content (string, optional)
142+
- `duplicate_of`: Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'. (number, optional)
143+
- `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)
144+
- `issue_number`: Issue number to update (number, optional)
145+
- `labels`: Labels to apply to this issue (string[], optional)
146+
- `method`: Write operation to perform on a single issue.
147+
Options are:
148+
- 'create' - creates a new issue.
149+
- 'update' - updates an existing issue.
150+
(string, required)
151+
- `milestone`: Milestone number (number, optional)
152+
- `owner`: Repository owner (string, required)
153+
- `repo`: Repository name (string, required)
154+
- `state`: New state (string, optional)
155+
- `state_reason`: Reason for the state change. Ignored unless state is changed. (string, optional)
156+
- `title`: Issue title (string, optional)
157+
- `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)
158+
159+
- **list_issues** - List issues
160+
- **Required OAuth Scopes**: `repo`
161+
- `after`: Cursor for pagination. Use the cursor from the previous response. (string, optional)
162+
- `direction`: Order direction. If provided, the 'orderBy' also needs to be provided. (string, optional)
163+
- `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)
164+
- `labels`: Filter by labels (string[], optional)
165+
- `orderBy`: Order issues by field. If provided, the 'direction' also needs to be provided. (string, optional)
166+
- `owner`: Repository owner (string, required)
167+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
168+
- `repo`: Repository name (string, required)
169+
- `since`: Filter by date (ISO 8601 timestamp) (string, optional)
170+
- `state`: Filter by state, by default both open and closed issues are returned when not provided (string, optional)
171+
136172
<!-- END AUTOMATED INSIDERS TOOLS -->
137173

138174
---
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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+
"idempotentHint": false,
13+
"readOnlyHint": false,
14+
"title": "Create or update issue/pull request"
15+
},
16+
"description": "Create a new or update an existing issue in a GitHub repository.",
17+
"inputSchema": {
18+
"properties": {
19+
"assignees": {
20+
"description": "Usernames to assign to this issue",
21+
"items": {
22+
"type": "string"
23+
},
24+
"type": "array"
25+
},
26+
"body": {
27+
"description": "Issue body content",
28+
"type": "string"
29+
},
30+
"duplicate_of": {
31+
"description": "Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'.",
32+
"type": "number"
33+
},
34+
"issue_fields": {
35+
"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'.",
36+
"items": {
37+
"additionalProperties": false,
38+
"properties": {
39+
"delete": {
40+
"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'.",
41+
"enum": [
42+
true
43+
],
44+
"type": "boolean"
45+
},
46+
"field_name": {
47+
"description": "Issue field name (case-insensitive). Must match a field returned by list_issue_fields for this repository or its organization.",
48+
"type": "string"
49+
},
50+
"field_option_name": {
51+
"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'.",
52+
"type": "string"
53+
},
54+
"field_option_names": {
55+
"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'.",
56+
"items": {
57+
"type": "string"
58+
},
59+
"type": "array"
60+
},
61+
"value": {
62+
"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'.",
63+
"type": [
64+
"string",
65+
"number",
66+
"boolean"
67+
]
68+
}
69+
},
70+
"required": [
71+
"field_name"
72+
],
73+
"type": "object"
74+
},
75+
"type": "array"
76+
},
77+
"issue_number": {
78+
"description": "Issue number to update",
79+
"type": "number"
80+
},
81+
"labels": {
82+
"description": "Labels to apply to this issue",
83+
"items": {
84+
"type": "string"
85+
},
86+
"type": "array"
87+
},
88+
"method": {
89+
"description": "Write operation to perform on a single issue.\nOptions are:\n- 'create' - creates a new issue.\n- 'update' - updates an existing issue.\n",
90+
"enum": [
91+
"create",
92+
"update"
93+
],
94+
"type": "string"
95+
},
96+
"milestone": {
97+
"description": "Milestone number",
98+
"type": "number"
99+
},
100+
"owner": {
101+
"description": "Repository owner",
102+
"type": "string"
103+
},
104+
"repo": {
105+
"description": "Repository name",
106+
"type": "string"
107+
},
108+
"state": {
109+
"description": "New state",
110+
"enum": [
111+
"open",
112+
"closed"
113+
],
114+
"type": "string"
115+
},
116+
"state_reason": {
117+
"description": "Reason for the state change. Ignored unless state is changed.",
118+
"enum": [
119+
"completed",
120+
"not_planned",
121+
"duplicate"
122+
],
123+
"type": "string"
124+
},
125+
"title": {
126+
"description": "Issue title",
127+
"type": "string"
128+
},
129+
"type": {
130+
"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.",
131+
"type": "string"
132+
}
133+
},
134+
"required": [
135+
"method",
136+
"owner",
137+
"repo"
138+
],
139+
"type": "object"
140+
},
141+
"name": "issue_write"
142+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"annotations": {
3+
"idempotentHint": false,
4+
"readOnlyHint": true,
5+
"title": "List issue fields"
6+
},
7+
"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.",
8+
"inputSchema": {
9+
"properties": {
10+
"owner": {
11+
"description": "The account owner of the repository or organization. The name is not case sensitive.",
12+
"type": "string"
13+
},
14+
"repo": {
15+
"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.",
16+
"type": "string"
17+
}
18+
},
19+
"required": [
20+
"owner"
21+
],
22+
"type": "object"
23+
},
24+
"name": "list_issue_fields"
25+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
{
2+
"annotations": {
3+
"idempotentHint": false,
4+
"readOnlyHint": true,
5+
"title": "List issues"
6+
},
7+
"description": "List issues in a GitHub repository. For pagination, use the 'endCursor' from the previous response's 'pageInfo' in the 'after' parameter.",
8+
"inputSchema": {
9+
"properties": {
10+
"after": {
11+
"description": "Cursor for pagination. Use the cursor from the previous response.",
12+
"type": "string"
13+
},
14+
"direction": {
15+
"description": "Order direction. If provided, the 'orderBy' also needs to be provided.",
16+
"enum": [
17+
"ASC",
18+
"DESC"
19+
],
20+
"type": "string"
21+
},
22+
"field_filters": {
23+
"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.",
24+
"items": {
25+
"properties": {
26+
"field_name": {
27+
"description": "Name of the custom field (e.g. \"Priority\"). Case-insensitive.",
28+
"type": "string"
29+
},
30+
"value": {
31+
"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'.",
32+
"type": "string"
33+
},
34+
"values": {
35+
"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'.",
36+
"items": {
37+
"type": "string"
38+
},
39+
"type": "array"
40+
}
41+
},
42+
"required": [
43+
"field_name"
44+
],
45+
"type": "object"
46+
},
47+
"type": "array"
48+
},
49+
"labels": {
50+
"description": "Filter by labels",
51+
"items": {
52+
"type": "string"
53+
},
54+
"type": "array"
55+
},
56+
"orderBy": {
57+
"description": "Order issues by field. If provided, the 'direction' also needs to be provided.",
58+
"enum": [
59+
"CREATED_AT",
60+
"UPDATED_AT",
61+
"COMMENTS"
62+
],
63+
"type": "string"
64+
},
65+
"owner": {
66+
"description": "Repository owner",
67+
"type": "string"
68+
},
69+
"perPage": {
70+
"description": "Results per page for pagination (min 1, max 100)",
71+
"maximum": 100,
72+
"minimum": 1,
73+
"type": "number"
74+
},
75+
"repo": {
76+
"description": "Repository name",
77+
"type": "string"
78+
},
79+
"since": {
80+
"description": "Filter by date (ISO 8601 timestamp)",
81+
"type": "string"
82+
},
83+
"state": {
84+
"description": "Filter by state, by default both open and closed issues are returned when not provided",
85+
"enum": [
86+
"OPEN",
87+
"CLOSED"
88+
],
89+
"type": "string"
90+
}
91+
},
92+
"required": [
93+
"owner",
94+
"repo"
95+
],
96+
"type": "object"
97+
},
98+
"name": "list_issues"
99+
}

pkg/github/__toolsnaps__/set_issue_fields.snap

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"inputSchema": {
1111
"properties": {
1212
"fields": {
13-
"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.",
13+
"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.",
1414
"items": {
1515
"properties": {
1616
"confidence": {
@@ -38,6 +38,13 @@
3838
"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.",
3939
"type": "boolean"
4040
},
41+
"multi_select_option_ids": {
42+
"description": "The GraphQL node IDs of the options to set for a multi select field",
43+
"items": {
44+
"type": "string"
45+
},
46+
"type": "array"
47+
},
4148
"number_value": {
4249
"description": "The value to set for a number field",
4350
"type": "number"

0 commit comments

Comments
 (0)