Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -855,7 +855,6 @@ The following sets of tools are available:
- `assignees`: Usernames to assign to this issue (string[], optional)
- `body`: Issue body content (string, optional)
- `duplicate_of`: Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'. (number, optional)
- `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)
- `issue_number`: Issue number to update (number, optional)
- `labels`: Labels to apply to this issue (string[], optional)
- `method`: Write operation to perform on a single issue.
Expand Down
22 changes: 21 additions & 1 deletion docs/feature-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ runtime behavior (such as output formatting) won't appear here.
- `assignees`: Usernames to assign to this issue (string[], optional)
- `body`: Issue body content (string, optional)
- `duplicate_of`: Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'. (number, optional)
- `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)
- `issue_number`: Issue number to update (number, optional)
- `labels`: Labels to apply to this issue (string[], optional)
- `method`: Write operation to perform on a single issue.
Expand All @@ -74,6 +73,27 @@ runtime behavior (such as output formatting) won't appear here.

### `remote_mcp_issue_fields`

- **issue_write** - Create or update issue
- **Required OAuth Scopes**: `repo`
- `assignees`: Usernames to assign to this issue (string[], optional)
- `body`: Issue body content (string, optional)
- `duplicate_of`: Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'. (number, optional)
- `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)
- `issue_number`: Issue number to update (number, optional)
- `labels`: Labels to apply to this issue (string[], optional)
- `method`: Write operation to perform on a single issue.
Options are:
- 'create' - creates a new issue.
- 'update' - updates an existing issue.
(string, required)
- `milestone`: Milestone number (number, optional)
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)
- `state`: New state (string, optional)
- `state_reason`: Reason for the state change. Ignored unless state is changed. (string, optional)
- `title`: Issue title (string, optional)
- `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)

- **list_issue_fields** - List issue fields
- **Required OAuth Scopes**: `repo`, `read:org`
- **Accepted OAuth Scopes**: `admin:org`, `read:org`, `repo`, `write:org`
Expand Down
22 changes: 21 additions & 1 deletion docs/insiders-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ The list below is generated from the Go source. It covers tool **inventory and s
- `assignees`: Usernames to assign to this issue (string[], optional)
- `body`: Issue body content (string, optional)
- `duplicate_of`: Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'. (number, optional)
- `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)
- `issue_number`: Issue number to update (number, optional)
- `labels`: Labels to apply to this issue (string[], optional)
- `method`: Write operation to perform on a single issue.
Expand All @@ -68,6 +67,27 @@ The list below is generated from the Go source. It covers tool **inventory and s

### `remote_mcp_issue_fields`

- **issue_write** - Create or update issue
- **Required OAuth Scopes**: `repo`
- `assignees`: Usernames to assign to this issue (string[], optional)
- `body`: Issue body content (string, optional)
- `duplicate_of`: Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'. (number, optional)
- `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)
- `issue_number`: Issue number to update (number, optional)
- `labels`: Labels to apply to this issue (string[], optional)
- `method`: Write operation to perform on a single issue.
Options are:
- 'create' - creates a new issue.
- 'update' - updates an existing issue.
(string, required)
- `milestone`: Milestone number (number, optional)
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)
- `state`: New state (string, optional)
- `state_reason`: Reason for the state change. Ignored unless state is changed. (string, optional)
- `title`: Issue title (string, optional)
- `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)

- **list_issue_fields** - List issue fields
- **Required OAuth Scopes**: `repo`, `read:org`
- **Accepted OAuth Scopes**: `admin:org`, `read:org`, `repo`, `write:org`
Expand Down
36 changes: 0 additions & 36 deletions pkg/github/__toolsnaps__/issue_write.snap
Original file line number Diff line number Diff line change
Expand Up @@ -29,42 +29,6 @@
"description": "Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'.",
"type": "number"
},
"issue_fields": {
"description": "Issue field values to set or clear. Each item requires 'field_name' and exactly one of 'value', 'field_option_name', or 'delete: true'.",
"items": {
"additionalProperties": false,
"properties": {
"delete": {
"description": "Set to true to clear this field's current value on the issue. Cannot be combined with 'value' or 'field_option_name'.",
"enum": [
true
],
"type": "boolean"
},
"field_name": {
"description": "Issue field name (case-insensitive). Must match a field returned by list_issue_fields for this repository or its organization.",
"type": "string"
},
"field_option_name": {
"description": "Option name for single-select fields. Validated against the field's options before the API call. Cannot be combined with 'value' or 'delete'.",
"type": "string"
},
"value": {
"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'.",
"type": [
"string",
"number",
"boolean"
]
}
},
"required": [
"field_name"
],
"type": "object"
},
"type": "array"
},
"issue_number": {
"description": "Issue number to update",
"type": "number"
Expand Down
133 changes: 133 additions & 0 deletions pkg/github/__toolsnaps__/issue_write_ff_remote_mcp_issue_fields.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
{
"_meta": {
"ui": {
"resourceUri": "ui://github-mcp-server/issue-write",
"visibility": [
"model",
"app"
]
}
},
"annotations": {
"title": "Create or update issue"
},
"description": "Create a new or update an existing issue in a GitHub repository.",
"inputSchema": {
"properties": {
"assignees": {
"description": "Usernames to assign to this issue",
"items": {
"type": "string"
},
"type": "array"
},
"body": {
"description": "Issue body content",
"type": "string"
},
"duplicate_of": {
"description": "Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'.",
"type": "number"
},
"issue_fields": {
"description": "Issue field values to set or clear. Each item requires 'field_name' and exactly one of 'value', 'field_option_name', or 'delete: true'.",
"items": {
"additionalProperties": false,
"properties": {
"delete": {
"description": "Set to true to clear this field's current value on the issue. Cannot be combined with 'value' or 'field_option_name'.",
"enum": [
true
],
"type": "boolean"
},
"field_name": {
"description": "Issue field name (case-insensitive). Must match a field returned by list_issue_fields for this repository or its organization.",
"type": "string"
},
"field_option_name": {
"description": "Option name for single-select fields. Validated against the field's options before the API call. Cannot be combined with 'value' or 'delete'.",
"type": "string"
},
"value": {
"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'.",
"type": [
"string",
"number",
"boolean"
]
}
},
"required": [
"field_name"
],
"type": "object"
},
"type": "array"
},
"issue_number": {
"description": "Issue number to update",
"type": "number"
},
"labels": {
"description": "Labels to apply to this issue",
"items": {
"type": "string"
},
"type": "array"
},
"method": {
"description": "Write operation to perform on a single issue.\nOptions are:\n- 'create' - creates a new issue.\n- 'update' - updates an existing issue.\n",
"enum": [
"create",
"update"
],
"type": "string"
},
"milestone": {
"description": "Milestone number",
"type": "number"
},
"owner": {
"description": "Repository owner",
"type": "string"
},
"repo": {
"description": "Repository name",
"type": "string"
},
"state": {
"description": "New state",
"enum": [
"open",
"closed"
],
"type": "string"
},
"state_reason": {
"description": "Reason for the state change. Ignored unless state is changed.",
"enum": [
"completed",
"not_planned",
"duplicate"
],
"type": "string"
},
"title": {
"description": "Issue title",
"type": "string"
},
"type": {
"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.",
"type": "string"
}
},
"required": [
"method",
"owner",
"repo"
],
"type": "object"
},
"name": "issue_write"
}
2 changes: 1 addition & 1 deletion pkg/github/csv_output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func TestCSVOutputAppliesToFlagGatedListTools(t *testing.T) {
enabledOnly := testCSVOutputTool("list_things", `[{"number":1}]`)
enabledOnly.FeatureFlagEnable = FeatureFlagIssueFields
disabledOnly := testCSVOutputTool("list_legacy_things", `[{"number":2}]`)
disabledOnly.FeatureFlagDisable = FeatureFlagIssueFields
disabledOnly.FeatureFlagDisable = []string{FeatureFlagIssueFields}

tools := withCSVOutput([]inventory.ServerTool{enabledOnly, disabledOnly})
require.Len(t, tools, 2)
Expand Down
70 changes: 53 additions & 17 deletions pkg/github/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -884,13 +884,16 @@ func GetIssue(ctx context.Context, client *github.Client, deps ToolDependencies,

minimalIssue := convertToMinimalIssue(issue)

// Enrich with field_values via GraphQL for consistency with list_issues/search_issues
if issue != nil && issue.NodeID != nil && *issue.NodeID != "" {
gqlClient, err := deps.GetGQLClient(ctx)
if err == nil {
if fieldValuesByID, err := fetchIssueFieldValuesByNodeID(ctx, gqlClient, []*github.Issue{issue}); err == nil {
minimalIssue.FieldValues = fieldValuesByID[*issue.NodeID]
minimalIssue.IssueFieldValues = nil // Clear verbose REST format
// Always drop the verbose REST IssueFieldValues; only enrich with the GraphQL
// field_values view when the issue-fields feature flag is on.
minimalIssue.IssueFieldValues = nil
if deps.IsFeatureEnabled(ctx, FeatureFlagIssueFields) {
if issue != nil && issue.NodeID != nil && *issue.NodeID != "" {
gqlClient, err := deps.GetGQLClient(ctx)
if err == nil {
if fieldValuesByID, err := fetchIssueFieldValuesByNodeID(ctx, gqlClient, []*github.Issue{issue}); err == nil {
minimalIssue.FieldValues = fieldValuesByID[*issue.NodeID]
}
}
}
}
Expand Down Expand Up @@ -1331,7 +1334,7 @@ Options are:
return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
}
})
st.FeatureFlagDisable = FeatureFlagIssuesGranular
st.FeatureFlagDisable = []string{FeatureFlagIssuesGranular}
return st
}

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

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

// IssueWrite is the FeatureFlagIssueFields-enabled variant of issue_write
// (with the issue_fields parameter). LegacyIssueWrite is served when the flag
// is off. Both register under the tool name "issue_write"; exactly one is
// active at a time via mutually exclusive feature-flag annotations. Delete the
// LegacyIssueWrite block (and the includeIssueFields parameter) when the flag
// is removed.
func IssueWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
st := buildIssueWrite(t, true)
st.FeatureFlagEnable = FeatureFlagIssueFields
st.FeatureFlagDisable = []string{FeatureFlagIssuesGranular}
return st
}

// LegacyIssueWrite is 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. Hidden whenever the granular toolset or
// the issue-fields flag is on.
func LegacyIssueWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
st := buildIssueWrite(t, false)
st.FeatureFlagDisable = []string{FeatureFlagIssuesGranular, FeatureFlagIssueFields}
return st
}

func buildIssueWrite(t translations.TranslationHelperFunc, includeIssueFields bool) inventory.ServerTool {
st := NewTool(
ToolsetMetadataIssues,
mcp.Tool{
Expand Down Expand Up @@ -1978,9 +2003,12 @@ Options are:
return utils.NewToolResultError("duplicate_of can only be used when state_reason is 'duplicate'"), nil, nil
}

issueFields, err := optionalIssueWriteFields(args)
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
var issueFields []issueWriteFieldInput
if includeIssueFields {
issueFields, err = optionalIssueWriteFields(args)
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
}

client, err := deps.GetClient(ctx)
Expand All @@ -1993,9 +2021,13 @@ Options are:
return utils.NewToolResultErrorFromErr("failed to get GraphQL client", err), nil, nil
}
Comment on lines 2003 to 2006

issueFieldValues, fieldIDsToDelete, err := resolveIssueRequestFieldValues(ctx, gqlClient, owner, repo, issueFields)
if err != nil {
return utils.NewToolResultError(fmt.Sprintf("failed to resolve issue_fields: %v", err)), nil, nil
var issueFieldValues []*github.IssueRequestFieldValue
var fieldIDsToDelete []int64
if len(issueFields) > 0 {
issueFieldValues, fieldIDsToDelete, err = resolveIssueRequestFieldValues(ctx, gqlClient, owner, repo, issueFields)
if err != nil {
return utils.NewToolResultError(fmt.Sprintf("failed to resolve issue_fields: %v", err)), nil, nil
}
}

switch method {
Expand All @@ -2013,7 +2045,11 @@ Options are:
return utils.NewToolResultError("invalid method, must be either 'create' or 'update'"), nil, nil
}
})
st.FeatureFlagDisable = FeatureFlagIssuesGranular
if !includeIssueFields {
if schema, ok := st.Tool.InputSchema.(*jsonschema.Schema); ok {
delete(schema.Properties, "issue_fields")
}
}
return st
}

Expand Down Expand Up @@ -2690,7 +2726,7 @@ func LegacyListIssues(t translations.TranslationHelperFunc) inventory.ServerTool
}
return result, nil, nil
})
st.FeatureFlagDisable = FeatureFlagIssueFields
st.FeatureFlagDisable = []string{FeatureFlagIssueFields}
return st
}

Expand Down
Loading
Loading