Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,7 @@ 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. Each item requires 'field_name' and exactly one of 'value' or 'field_option_name'. (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
2 changes: 2 additions & 0 deletions docs/feature-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ 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. Each item requires 'field_name' and exactly one of 'value' or 'field_option_name'. (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 Expand Up @@ -177,6 +178,7 @@ runtime behavior (such as output formatting) won't appear here.

- **update_issue_type** - Update Issue Type
- **Required OAuth Scopes**: `repo`
- `is_suggestion`: If true, propose the issue type change instead of applying it. Defaults to false, which applies the change to the issue. (boolean, optional)
- `issue_number`: The issue number to update (number, required)
- `issue_type`: The issue type to set (string, required)
- `owner`: Repository owner (username or organization) (string, required)
Expand Down
1 change: 1 addition & 0 deletions docs/insiders-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ 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. Each item requires 'field_name' and exactly one of 'value' or 'field_option_name'. (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
51 changes: 51 additions & 0 deletions pkg/github/__toolsnaps__/issue_write.snap
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,57 @@
"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. Each item requires 'field_name' and exactly one of 'value' or 'field_option_name'.",
"items": {
"additionalProperties": false,
"oneOf": [
{
"not": {
"required": [
"field_option_name"
]
},
"required": [
"value"
]
},
{
"not": {
"required": [
"value"
]
},
"required": [
"field_option_name"
]
}
],
"properties": {
"field_name": {
"description": "Issue field name",
"type": "string"
},
"field_option_name": {
"description": "Option name for single-select fields — validates the option exists in the field definition before setting it.",
"type": "string"
},
"value": {
"description": "Value to set. For single-select fields, prefer 'field_option_name' to validate the option exists first.",
"type": [
"string",
"number",
"boolean"
]
}
},
"required": [
"field_name"
],
"type": "object"
},
"type": "array"
},
"issue_number": {
"description": "Issue number to update",
"type": "number"
Expand Down
67 changes: 46 additions & 21 deletions pkg/github/issue_fields.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"strconv"

ghcontext "github.com/github/github-mcp-server/pkg/context"
ghErrors "github.com/github/github-mcp-server/pkg/errors"
Expand All @@ -19,6 +20,7 @@ import (
// IssueField represents a repository issue field definition.
type IssueField struct {
ID string `json:"id"`
DatabaseID int64 `json:"full_database_id,omitempty"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
DataType string `json:"data_type"`
Expand All @@ -37,36 +39,42 @@ type IssueSingleSelectFieldOption struct {

// issueFieldNode is the GraphQL fragment for a single issue field in the IssueFields union.
// Only the fragment matching __typename is populated; read from the matching fragment.
// fullDatabaseId (BigInt scalar, returned as string) is fetched on each concrete type because
// shurcooL/githubv4 does not support interface fragments at the top level of a union.
type issueFieldNode struct {
TypeName githubv4.String `graphql:"__typename"`
IssueFieldText struct {
ID githubv4.ID
Name githubv4.String
Description githubv4.String
DataType githubv4.String
Visibility githubv4.String
ID githubv4.ID
FullDatabaseID githubv4.String `graphql:"fullDatabaseId"`
Name githubv4.String
Description githubv4.String
DataType githubv4.String
Visibility githubv4.String
} `graphql:"... on IssueFieldText"`
IssueFieldNumber struct {
ID githubv4.ID
Name githubv4.String
Description githubv4.String
DataType githubv4.String
Visibility githubv4.String
ID githubv4.ID
FullDatabaseID githubv4.String `graphql:"fullDatabaseId"`
Name githubv4.String
Description githubv4.String
DataType githubv4.String
Visibility githubv4.String
} `graphql:"... on IssueFieldNumber"`
IssueFieldDate struct {
ID githubv4.ID
Name githubv4.String
Description githubv4.String
DataType githubv4.String
Visibility githubv4.String
ID githubv4.ID
FullDatabaseID githubv4.String `graphql:"fullDatabaseId"`
Name githubv4.String
Description githubv4.String
DataType githubv4.String
Visibility githubv4.String
} `graphql:"... on IssueFieldDate"`
IssueFieldSingleSelect struct {
ID githubv4.ID
Name githubv4.String
Description githubv4.String
DataType githubv4.String
Visibility githubv4.String
Options []struct {
ID githubv4.ID
FullDatabaseID githubv4.String `graphql:"fullDatabaseId"`
Name githubv4.String
Description githubv4.String
DataType githubv4.String
Visibility githubv4.String
Options []struct {
ID githubv4.ID
Name githubv4.String
Description githubv4.String
Expand Down Expand Up @@ -200,6 +208,7 @@ func issueFieldsFromNodes(nodes []issueFieldNode) []IssueField {
}
f = IssueField{
ID: fmt.Sprintf("%v", node.IssueFieldSingleSelect.ID),
DatabaseID: parseFullDatabaseID(string(node.IssueFieldSingleSelect.FullDatabaseID)),
Name: string(node.IssueFieldSingleSelect.Name),
Description: string(node.IssueFieldSingleSelect.Description),
DataType: string(node.IssueFieldSingleSelect.DataType),
Expand All @@ -209,6 +218,7 @@ func issueFieldsFromNodes(nodes []issueFieldNode) []IssueField {
case "IssueFieldText":
f = IssueField{
ID: fmt.Sprintf("%v", node.IssueFieldText.ID),
DatabaseID: parseFullDatabaseID(string(node.IssueFieldText.FullDatabaseID)),
Name: string(node.IssueFieldText.Name),
Description: string(node.IssueFieldText.Description),
DataType: string(node.IssueFieldText.DataType),
Expand All @@ -217,6 +227,7 @@ func issueFieldsFromNodes(nodes []issueFieldNode) []IssueField {
case "IssueFieldNumber":
f = IssueField{
ID: fmt.Sprintf("%v", node.IssueFieldNumber.ID),
DatabaseID: parseFullDatabaseID(string(node.IssueFieldNumber.FullDatabaseID)),
Name: string(node.IssueFieldNumber.Name),
Description: string(node.IssueFieldNumber.Description),
DataType: string(node.IssueFieldNumber.DataType),
Expand All @@ -225,6 +236,7 @@ func issueFieldsFromNodes(nodes []issueFieldNode) []IssueField {
case "IssueFieldDate":
f = IssueField{
ID: fmt.Sprintf("%v", node.IssueFieldDate.ID),
DatabaseID: parseFullDatabaseID(string(node.IssueFieldDate.FullDatabaseID)),
Name: string(node.IssueFieldDate.Name),
Description: string(node.IssueFieldDate.Description),
DataType: string(node.IssueFieldDate.DataType),
Expand All @@ -237,3 +249,16 @@ func issueFieldsFromNodes(nodes []issueFieldNode) []IssueField {
}
return fields
}

// parseFullDatabaseID converts a BigInt scalar string (e.g. "12345") to int64.
// Returns 0 if the string is empty or cannot be parsed.
func parseFullDatabaseID(s string) int64 {
if s == "" {
return 0
}
n, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return 0
}
return n
}
68 changes: 38 additions & 30 deletions pkg/github/issue_fields_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,13 @@ func Test_ListIssueFields(t *testing.T) {
"issueFields": map[string]any{
"nodes": []any{
map[string]any{
"__typename": "IssueFieldText",
"id": "IFT_1",
"name": "DRI",
"description": "Directly responsible individual",
"dataType": "TEXT",
"visibility": "ORG_ONLY",
"__typename": "IssueFieldText",
"id": "IFT_1",
"fullDatabaseId": "42",
"name": "DRI",
"description": "Directly responsible individual",
"dataType": "TEXT",
"visibility": "ORG_ONLY",
},
},
},
Expand All @@ -89,6 +90,7 @@ func Test_ListIssueFields(t *testing.T) {
expectedFields: []IssueField{
{
ID: "IFT_1",
DatabaseID: 42,
Name: "DRI",
Description: "Directly responsible individual",
DataType: "TEXT",
Expand All @@ -107,12 +109,13 @@ func Test_ListIssueFields(t *testing.T) {
"issueFields": map[string]any{
"nodes": []any{
map[string]any{
"__typename": "IssueFieldSingleSelect",
"id": "IFSS_1",
"name": "Priority",
"description": "Level of importance",
"dataType": "SINGLE_SELECT",
"visibility": "ALL",
"__typename": "IssueFieldSingleSelect",
"id": "IFSS_1",
"fullDatabaseId": "99",
"name": "Priority",
"description": "Level of importance",
"dataType": "SINGLE_SELECT",
"visibility": "ALL",
"options": []any{
map[string]any{
"id": "OPT_1",
Expand All @@ -133,6 +136,7 @@ func Test_ListIssueFields(t *testing.T) {
expectedFields: []IssueField{
{
ID: "IFSS_1",
DatabaseID: 99,
Name: "Priority",
Description: "Level of importance",
DataType: "SINGLE_SELECT",
Expand Down Expand Up @@ -165,18 +169,19 @@ func Test_ListIssueFields(t *testing.T) {
"issueFields": map[string]any{
"nodes": []any{
map[string]any{
"__typename": "IssueFieldText",
"id": "IFT_1",
"name": "DRI",
"dataType": "TEXT",
"visibility": "ORG_ONLY",
"__typename": "IssueFieldText",
"id": "IFT_1",
"fullDatabaseId": "77",
"name": "DRI",
"dataType": "TEXT",
"visibility": "ORG_ONLY",
},
},
},
},
}),
expectedFields: []IssueField{
{ID: "IFT_1", Name: "DRI", DataType: "TEXT", Visibility: "ORG_ONLY"},
{ID: "IFT_1", DatabaseID: 77, Name: "DRI", DataType: "TEXT", Visibility: "ORG_ONLY"},
},
},
{
Expand All @@ -190,18 +195,19 @@ func Test_ListIssueFields(t *testing.T) {
"issueFields": map[string]any{
"nodes": []any{
map[string]any{
"__typename": "IssueFieldNumber",
"id": "IFN_1",
"name": "Engineering Staffing",
"dataType": "NUMBER",
"visibility": "ORG_ONLY",
"__typename": "IssueFieldNumber",
"id": "IFN_1",
"fullDatabaseId": "101",
"name": "Engineering Staffing",
"dataType": "NUMBER",
"visibility": "ORG_ONLY",
},
},
},
},
}),
expectedFields: []IssueField{
{ID: "IFN_1", Name: "Engineering Staffing", DataType: "NUMBER", Visibility: "ORG_ONLY"},
{ID: "IFN_1", DatabaseID: 101, Name: "Engineering Staffing", DataType: "NUMBER", Visibility: "ORG_ONLY"},
},
},
{
Expand All @@ -215,18 +221,19 @@ func Test_ListIssueFields(t *testing.T) {
"issueFields": map[string]any{
"nodes": []any{
map[string]any{
"__typename": "IssueFieldDate",
"id": "IFD_1",
"name": "Target Date",
"dataType": "DATE",
"visibility": "ORG_ONLY",
"__typename": "IssueFieldDate",
"id": "IFD_1",
"fullDatabaseId": "202",
"name": "Target Date",
"dataType": "DATE",
"visibility": "ORG_ONLY",
},
},
},
},
}),
expectedFields: []IssueField{
{ID: "IFD_1", Name: "Target Date", DataType: "DATE", Visibility: "ORG_ONLY"},
{ID: "IFD_1", DatabaseID: 202, Name: "Target Date", DataType: "DATE", Visibility: "ORG_ONLY"},
},
},
{
Expand Down Expand Up @@ -284,6 +291,7 @@ func Test_ListIssueFields(t *testing.T) {
require.Equal(t, len(tc.expectedFields), len(returnedFields))
for i, expected := range tc.expectedFields {
assert.Equal(t, expected.ID, returnedFields[i].ID)
assert.Equal(t, expected.DatabaseID, returnedFields[i].DatabaseID)
assert.Equal(t, expected.Name, returnedFields[i].Name)
assert.Equal(t, expected.DataType, returnedFields[i].DataType)
assert.Equal(t, expected.Visibility, returnedFields[i].Visibility)
Expand Down
Loading
Loading