Skip to content

Commit e7f7bb8

Browse files
authored
Support removing issue types (#2999)
* Render union types in generated docs Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ea8faa5c-7f26-4e2d-bf9c-6f0b5f173e8c * Support clearing issue types with issue_write Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ea8faa5c-7f26-4e2d-bf9c-6f0b5f173e8c * Support clearing issue types with granular tool Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ea8faa5c-7f26-4e2d-bf9c-6f0b5f173e8c * Validate duplicate closures before updates Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ea8faa5c-7f26-4e2d-bf9c-6f0b5f173e8c --------- Copilot-Session: ea8faa5c-7f26-4e2d-bf9c-6f0b5f173e8c
1 parent f3cb662 commit e7f7bb8

14 files changed

Lines changed: 388 additions & 51 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -926,7 +926,7 @@ The following sets of tools are available:
926926
- **Required OAuth Scopes**: `repo`
927927
- `assignees`: Usernames to assign to this issue (string[], optional)
928928
- `body`: Issue body content (string, optional)
929-
- `duplicate_of`: Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'. (number, optional)
929+
- `duplicate_of`: Issue number that this issue is a duplicate of. Required when state_reason is 'duplicate'. (number, optional)
930930
- `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)
931931
- `issue_number`: Issue number to update (number, optional)
932932
- `labels`: Labels to apply to this issue (string[], optional)
@@ -941,7 +941,7 @@ The following sets of tools are available:
941941
- `state`: New state (string, optional)
942942
- `state_reason`: Reason for the state change. Ignored unless state is changed. (string, optional)
943943
- `title`: Issue title (string, optional)
944-
- `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)
944+
- `type`: Type of this issue. For updates, pass null to remove the current type. Only use if issue types are enabled for this repository. Use list_issue_types to get valid type values for this repository or its owner organization. If the repository doesn't support issue types, omit this parameter. (string | null, optional)
945945

946946
- **list_issue_fields** - List issue fields
947947
- **Required OAuth Scopes (any of)**: `repo`, `read:org`

cmd/github-mcp-server/generate_docs.go

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -273,19 +273,7 @@ func writeToolDoc(buf *strings.Builder, tool inventory.ServerTool) {
273273
requiredStr = "required"
274274
}
275275

276-
var typeStr string
277-
278-
// Get the type and description
279-
switch prop.Type {
280-
case "array":
281-
if prop.Items != nil {
282-
typeStr = prop.Items.Type + "[]"
283-
} else {
284-
typeStr = "array"
285-
}
286-
default:
287-
typeStr = prop.Type
288-
}
276+
typeStr := schemaTypeString(prop)
289277

290278
// Indent any continuation lines in the description to maintain markdown formatting
291279
description := indentMultilineDescription(prop.Description, " ")
@@ -300,6 +288,40 @@ func writeToolDoc(buf *strings.Builder, tool inventory.ServerTool) {
300288
}
301289
}
302290

291+
func schemaTypeString(schema *jsonschema.Schema) string {
292+
switch {
293+
case schema.Type == "array":
294+
if schema.Items != nil {
295+
return schema.Items.Type + "[]"
296+
}
297+
return "array"
298+
case schema.Type != "":
299+
return schema.Type
300+
case len(schema.Types) > 0:
301+
return strings.Join(schema.Types, " | ")
302+
}
303+
304+
var union []*jsonschema.Schema
305+
switch {
306+
case len(schema.AnyOf) > 0:
307+
union = schema.AnyOf
308+
case len(schema.OneOf) > 0:
309+
union = schema.OneOf
310+
default:
311+
// A schema without type constraints accepts any value.
312+
return "any"
313+
}
314+
315+
types := make([]string, 0, len(union))
316+
for _, member := range union {
317+
memberType := schemaTypeString(member)
318+
if !slices.Contains(types, memberType) {
319+
types = append(types, memberType)
320+
}
321+
}
322+
return strings.Join(types, " | ")
323+
}
324+
303325
// scopesEqual checks if two scope slices contain the same elements (order-independent)
304326
func scopesEqual(a, b []string) bool {
305327
if len(a) != len(b) {

cmd/github-mcp-server/main_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"path/filepath"
66
"testing"
77

8+
"github.com/google/jsonschema-go/jsonschema"
89
"github.com/stretchr/testify/assert"
910
"github.com/stretchr/testify/require"
1011
)
@@ -36,3 +37,29 @@ func TestGitHubAppFlagsAreStdioOnly(t *testing.T) {
3637
assert.NotNil(t, stdioCmd.Flags().Lookup("app-id"))
3738
assert.Nil(t, httpCmd.Flags().Lookup("app-id"))
3839
}
40+
41+
func TestSchemaTypeString(t *testing.T) {
42+
tests := []struct {
43+
name string
44+
schema *jsonschema.Schema
45+
want string
46+
}{
47+
{name: "type", schema: &jsonschema.Schema{Type: "string"}, want: "string"},
48+
{name: "types", schema: &jsonschema.Schema{Types: []string{"string", "number"}}, want: "string | number"},
49+
{name: "unconstrained", schema: &jsonschema.Schema{}, want: "any"},
50+
{name: "anyOf", schema: &jsonschema.Schema{AnyOf: []*jsonschema.Schema{{Type: "string"}, {Type: "null"}}}, want: "string | null"},
51+
{name: "oneOf", schema: &jsonschema.Schema{OneOf: []*jsonschema.Schema{{Type: "number"}, {Type: "string"}}}, want: "number | string"},
52+
{
53+
name: "array",
54+
schema: &jsonschema.Schema{Type: "array", Items: &jsonschema.Schema{Type: "string"}},
55+
want: "string[]",
56+
},
57+
{name: "untyped array", schema: &jsonschema.Schema{Type: "array"}, want: "array"},
58+
}
59+
60+
for _, tc := range tests {
61+
t.Run(tc.name, func(t *testing.T) {
62+
assert.Equal(t, tc.want, schemaTypeString(tc.schema))
63+
})
64+
}
65+
}

docs/feature-flags.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ runtime behavior (such as output formatting) won't appear here.
5656
- **MCP App UI**: `ui://github-mcp-server/issue-write`
5757
- `assignees`: Usernames to assign to this issue (string[], optional)
5858
- `body`: Issue body content (string, optional)
59-
- `duplicate_of`: Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'. (number, optional)
59+
- `duplicate_of`: Issue number that this issue is a duplicate of. Required when state_reason is 'duplicate'. (number, optional)
6060
- `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)
6161
- `issue_number`: Issue number to update (number, optional)
6262
- `labels`: Labels to apply to this issue (string[], optional)
@@ -71,7 +71,7 @@ runtime behavior (such as output formatting) won't appear here.
7171
- `state`: New state (string, optional)
7272
- `state_reason`: Reason for the state change. Ignored unless state is changed. (string, optional)
7373
- `title`: Issue title (string, optional)
74-
- `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)
74+
- `type`: Type of this issue. For updates, pass null to remove the current type. Only use if issue types are enabled for this repository. Use list_issue_types to get valid type values for this repository or its owner organization. If the repository doesn't support issue types, omit this parameter. (string | null, optional)
7575

7676
- **ui_get** - Get UI data
7777
- **Required OAuth Scopes (any of)**: `repo`, `read:org`
@@ -200,7 +200,7 @@ runtime behavior (such as output formatting) won't appear here.
200200
- `confidence`: How confident you are in this choice. Use 'HIGH' for clear signal or explicit user request, 'MEDIUM' for reasonable inference with some ambiguity, 'LOW' for best guess with limited signal. (string, optional)
201201
- `is_suggestion`: If true, this issue type change is sent to the API as a suggestion (suggest:true) rather than an applied value. Whether the type is applied or recorded as a proposal is determined by the API. (boolean, optional)
202202
- `issue_number`: The issue number to update (number, required)
203-
- `issue_type`: The issue type to set (string, required)
203+
- `issue_type`: The issue type to set, or null to remove the current type (string | null, required)
204204
- `owner`: Repository owner (username or organization) (string, required)
205205
- `rationale`: One concise sentence explaining what specifically about the issue led you to choose this type. State the concrete signal (e.g. 'Reports a crash when saving' → bug, 'Asks for dark mode support' → feature). (string, optional)
206206
- `repo`: Repository name (string, required)

docs/insiders-features.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ The list below is generated from the Go source. It covers tool **inventory and s
5050
- **MCP App UI**: `ui://github-mcp-server/issue-write`
5151
- `assignees`: Usernames to assign to this issue (string[], optional)
5252
- `body`: Issue body content (string, optional)
53-
- `duplicate_of`: Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'. (number, optional)
53+
- `duplicate_of`: Issue number that this issue is a duplicate of. Required when state_reason is 'duplicate'. (number, optional)
5454
- `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)
5555
- `issue_number`: Issue number to update (number, optional)
5656
- `labels`: Labels to apply to this issue (string[], optional)
@@ -65,7 +65,7 @@ The list below is generated from the Go source. It covers tool **inventory and s
6565
- `state`: New state (string, optional)
6666
- `state_reason`: Reason for the state change. Ignored unless state is changed. (string, optional)
6767
- `title`: Issue title (string, optional)
68-
- `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)
68+
- `type`: Type of this issue. For updates, pass null to remove the current type. Only use if issue types are enabled for this repository. Use list_issue_types to get valid type values for this repository or its owner organization. If the repository doesn't support issue types, omit this parameter. (string | null, optional)
6969

7070
- **ui_get** - Get UI data
7171
- **Required OAuth Scopes (any of)**: `repo`, `read:org`

pkg/github/__toolsnaps__/issue_write.snap

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"type": "string"
2929
},
3030
"duplicate_of": {
31-
"description": "Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'.",
31+
"description": "Issue number that this issue is a duplicate of. Required when state_reason is 'duplicate'.",
3232
"type": "number"
3333
},
3434
"issue_fields": {
@@ -120,8 +120,16 @@
120120
"type": "string"
121121
},
122122
"type": {
123-
"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.",
124-
"type": "string"
123+
"anyOf": [
124+
{
125+
"minLength": 1,
126+
"type": "string"
127+
},
128+
{
129+
"type": "null"
130+
}
131+
],
132+
"description": "Type of this issue. For updates, pass null to remove the current type. Only use if issue types are enabled for this repository. Use list_issue_types to get valid type values for this repository or its owner organization. If the repository doesn't support issue types, omit this parameter."
125133
}
126134
},
127135
"required": [

pkg/github/__toolsnaps__/update_issue_type.snap

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"readOnlyHint": false,
77
"title": "Update Issue Type"
88
},
9-
"description": "Update the type of an existing issue (e.g. 'bug', 'feature'). When setting values, include a confidence level (LOW, MEDIUM, or HIGH) reflecting how certain you are about the choice.",
9+
"description": "Set or remove the type of an existing issue. Pass null to remove the current type. When setting a value, include a confidence level (LOW, MEDIUM, or HIGH) reflecting how certain you are about the choice.",
1010
"inputSchema": {
1111
"properties": {
1212
"confidence": {
@@ -28,8 +28,16 @@
2828
"type": "number"
2929
},
3030
"issue_type": {
31-
"description": "The issue type to set",
32-
"type": "string"
31+
"anyOf": [
32+
{
33+
"minLength": 1,
34+
"type": "string"
35+
},
36+
{
37+
"type": "null"
38+
}
39+
],
40+
"description": "The issue type to set, or null to remove the current type"
3341
},
3442
"owner": {
3543
"description": "Repository owner (username or organization)",

pkg/github/granular_tools_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package github
33
import (
44
"context"
55
"encoding/json"
6+
"maps"
67
"net/http"
78
"strings"
89
"testing"
@@ -787,6 +788,18 @@ func TestGranularUpdateIssueType(t *testing.T) {
787788
},
788789
},
789790
},
791+
{
792+
name: "remove type with null",
793+
requestArgs: map[string]any{
794+
"owner": "owner",
795+
"repo": "repo",
796+
"issue_number": float64(1),
797+
"issue_type": nil,
798+
},
799+
expectedReq: map[string]any{
800+
"type": nil,
801+
},
802+
},
790803
}
791804

792805
for _, tc := range tests {
@@ -807,6 +820,45 @@ func TestGranularUpdateIssueType(t *testing.T) {
807820
}
808821
}
809822

823+
func TestGranularUpdateIssueTypeRejectsInvalidInput(t *testing.T) {
824+
tests := []struct {
825+
name string
826+
args map[string]any
827+
omitType bool
828+
wantError string
829+
}{
830+
{name: "missing type", omitType: true, wantError: "missing required parameter: issue_type"},
831+
{name: "empty type", args: map[string]any{"issue_type": ""}, wantError: "parameter issue_type must not be empty"},
832+
{name: "null with rationale", args: map[string]any{"rationale": "live validation"}, wantError: "suggestion metadata is not supported"},
833+
{name: "null with confidence", args: map[string]any{"confidence": "HIGH"}, wantError: "suggestion metadata is not supported"},
834+
{name: "null suggestion", args: map[string]any{"is_suggestion": true}, wantError: "suggestion metadata is not supported"},
835+
}
836+
837+
for _, tc := range tests {
838+
t.Run(tc.name, func(t *testing.T) {
839+
deps := BaseDeps{}
840+
serverTool := GranularUpdateIssueType(translations.NullTranslationHelper)
841+
handler := serverTool.Handler(deps)
842+
args := map[string]any{
843+
"owner": "owner",
844+
"repo": "repo",
845+
"issue_number": float64(1),
846+
"issue_type": nil,
847+
}
848+
if tc.omitType {
849+
delete(args, "issue_type")
850+
}
851+
maps.Copy(args, tc.args)
852+
request := createMCPRequest(args)
853+
854+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
855+
require.NoError(t, err)
856+
errorContent := getErrorResult(t, result)
857+
assert.Contains(t, errorContent.Text, tc.wantError)
858+
})
859+
}
860+
}
861+
810862
func TestGranularUpdateIssueTypeSuggest(t *testing.T) {
811863
tests := []struct {
812864
name string

0 commit comments

Comments
 (0)