Skip to content

Commit 27d07c5

Browse files
committed
Narrow Issue Field changes
Restore unrelated schema and standard batch behavior, isolate Issue Field helpers, and consolidate acceptance coverage without changing supported writes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1d574277-3735-4356-a753-8354aa6b537c
1 parent 54f1d0b commit 27d07c5

11 files changed

Lines changed: 539 additions & 909 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1133,7 +1133,7 @@ The following sets of tools are available:
11331133
- `status`: The status of the project. Used for 'create_project_status_update' method. (string, optional)
11341134
- `target_date`: The target date of the status update in YYYY-MM-DD format. Used for 'create_project_status_update' method. (string, optional)
11351135
- `title`: The project title. Required for 'create_project' method. (string, optional)
1136-
- `updated_field`: The Project or attached Issue Field to update and its new value. Required for 'update_project_item' and 'update_project_items'. For 'update_project_items', one top-level field/value applies to every item. Set value to null to clear the field. (object, optional)
1136+
- `updated_field`: The field/value to apply, using {"id": 123, "value": ...} or {"name": "Status", "value": ...}; null clears the field. Required for 'update_project_item' and 'update_project_items', where one top-level field/value applies to every item in a batch. (object, optional)
11371137

11381138
</details>
11391139

pkg/github/__toolsnaps__/projects_write.snap

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,17 +186,17 @@
186186
"type": "string"
187187
},
188188
"updated_field": {
189-
"description": "The Project or attached Issue Field to update and its new value. Required for 'update_project_item' and 'update_project_items'. For 'update_project_items', one top-level field/value applies to every item. Set value to null to clear the field.",
189+
"description": "The field/value to apply, using {\"id\": 123, \"value\": ...} or {\"name\": \"Status\", \"value\": ...}; null clears the field. Required for 'update_project_item' and 'update_project_items', where one top-level field/value applies to every item in a batch.",
190190
"oneOf": [
191191
{
192192
"additionalProperties": false,
193193
"properties": {
194194
"id": {
195-
"description": "The numeric Project field ID.",
195+
"description": "The numeric project field ID.",
196196
"type": "integer"
197197
},
198198
"value": {
199-
"description": "The field value."
199+
"description": "The value to apply. Any JSON value is accepted; use null to clear the field."
200200
}
201201
},
202202
"required": [
@@ -209,11 +209,11 @@
209209
"additionalProperties": false,
210210
"properties": {
211211
"name": {
212-
"description": "The case-insensitive Project or attached Issue Field name.",
212+
"description": "The project field name. Matching is case-insensitive.",
213213
"type": "string"
214214
},
215215
"value": {
216-
"description": "The field value."
216+
"description": "The value to apply. Any JSON value is accepted; use null to clear the field."
217217
}
218218
},
219219
"required": [

pkg/github/granular_tools_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2428,7 +2428,7 @@ func TestGranularSetIssueFields(t *testing.T) {
24282428
require.False(t, result.IsError, getTextResult(t, result).Text)
24292429
// The last request captured is the mutation; the preceding issue ID
24302430
// query does not require the feature flag.
2431-
assert.Equal(t, "issue_fields, repo_issue_fields, update_issue_suggestions", spy.captured.Get(headers.GraphQLFeaturesHeader))
2431+
assert.Equal(t, "update_issue_suggestions", spy.captured.Get(headers.GraphQLFeaturesHeader))
24322432
})
24332433
}
24342434

pkg/github/issues_granular.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,7 +1294,6 @@ func SetIssueFieldValues(ctx context.Context, gqlClient *githubv4.Client, issueI
12941294
IssueID: issueID,
12951295
IssueFields: issueFields,
12961296
}
1297-
ctx = ghcontext.WithGraphQLFeatures(ctx, "issue_fields", "repo_issue_fields", "update_issue_suggestions")
12981297
if err := gqlClient.Mutate(ctx, &mutation, input, nil); err != nil {
12991298
return MinimalResponse{}, err
13001299
}
@@ -1527,7 +1526,8 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv
15271526
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to get issue", err), nil, nil
15281527
}
15291528

1530-
response, err := SetIssueFieldValues(ctx, gqlClient, issueID, issueFields)
1529+
ctxWithFeatures := ghcontext.WithGraphQLFeatures(ctx, "update_issue_suggestions")
1530+
response, err := SetIssueFieldValues(ctxWithFeatures, gqlClient, issueID, issueFields)
15311531
if err != nil {
15321532
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to set issue field values", err), nil, nil
15331533
}

pkg/github/projects.go

Lines changed: 9 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strconv"
1111
"time"
1212

13+
ghcontext "github.com/github/github-mcp-server/pkg/context"
1314
ghErrors "github.com/github/github-mcp-server/pkg/errors"
1415
"github.com/github/github-mcp-server/pkg/ifc"
1516
"github.com/github/github-mcp-server/pkg/inventory"
@@ -536,7 +537,9 @@ func updateProjectItemsItemSchema() *jsonschema.Schema {
536537
}
537538

538539
func projectUpdatedFieldSchema() *jsonschema.Schema {
539-
value := &jsonschema.Schema{Description: "The field value."}
540+
value := &jsonschema.Schema{
541+
Description: "The value to apply. Any JSON value is accepted; use null to clear the field.",
542+
}
540543
variant := func(required []string, properties map[string]*jsonschema.Schema) *jsonschema.Schema {
541544
properties["value"] = value
542545
return &jsonschema.Schema{
@@ -549,18 +552,18 @@ func projectUpdatedFieldSchema() *jsonschema.Schema {
549552

550553
return &jsonschema.Schema{
551554
Type: "object",
552-
Description: "The Project or attached Issue Field to update and its new value. Required for 'update_project_item' and 'update_project_items'. For 'update_project_items', one top-level field/value applies to every item. Set value to null to clear the field.",
555+
Description: "The field/value to apply, using {\"id\": 123, \"value\": ...} or {\"name\": \"Status\", \"value\": ...}; null clears the field. Required for 'update_project_item' and 'update_project_items', where one top-level field/value applies to every item in a batch.",
553556
OneOf: []*jsonschema.Schema{
554557
variant([]string{"id", "value"}, map[string]*jsonschema.Schema{
555558
"id": {
556559
Type: "integer",
557-
Description: "The numeric Project field ID.",
560+
Description: "The numeric project field ID.",
558561
},
559562
}),
560563
variant([]string{"name", "value"}, map[string]*jsonschema.Schema{
561564
"name": {
562565
Type: "string",
563-
Description: "The case-insensitive Project or attached Issue Field name.",
566+
Description: "The project field name. Matching is case-insensitive.",
564567
},
565568
}),
566569
},
@@ -1267,7 +1270,8 @@ func updateProjectItem(ctx context.Context, client *github.Client, gqlClient *gi
12671270
}
12681271
return utils.NewToolResultError(resolveErr.Error()), nil, nil
12691272
}
1270-
response, mutationErr := SetIssueFieldValues(ctx, gqlClient, issueID, []IssueFieldCreateOrUpdateInput{*update.IssueField})
1273+
mutationCtx := ghcontext.WithGraphQLFeatures(ctx, "issue_fields", "repo_issue_fields", "update_issue_suggestions")
1274+
response, mutationErr := SetIssueFieldValues(mutationCtx, gqlClient, issueID, []IssueFieldCreateOrUpdateInput{*update.IssueField})
12711275
if mutationErr != nil {
12721276
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to update Issue Field", mutationErr), nil, nil
12731277
}
@@ -1311,36 +1315,6 @@ func updateProjectItem(ctx context.Context, client *github.Client, gqlClient *gi
13111315
return utils.NewToolResultText(string(r)), nil, nil
13121316
}
13131317

1314-
func projectItemIssueNodeID(item *github.ProjectV2Item) (githubv4.ID, error) {
1315-
if item == nil || item.ContentType == nil {
1316-
return "", ghErrors.NewStructuredResolutionError(
1317-
"issue_field_metadata_unavailable",
1318-
"",
1319-
"the project item response did not identify its content type; Issue Fields can only be updated on Issue items",
1320-
nil,
1321-
)
1322-
}
1323-
1324-
contentType := string(*item.ContentType)
1325-
if contentType != "Issue" {
1326-
return "", ghErrors.NewStructuredResolutionError(
1327-
"unsupported_item_type",
1328-
contentType,
1329-
"Issue Fields can only be updated on Issue project items, not pull requests or draft issues",
1330-
nil,
1331-
)
1332-
}
1333-
if item.Content == nil || item.Content.Issue == nil || item.Content.Issue.GetNodeID() == "" {
1334-
return "", ghErrors.NewStructuredResolutionError(
1335-
"issue_field_metadata_unavailable",
1336-
"Issue",
1337-
"the project item response did not include the underlying Issue node ID needed to update the Issue Field",
1338-
nil,
1339-
)
1340-
}
1341-
return githubv4.ID(item.Content.Issue.GetNodeID()), nil
1342-
}
1343-
13441318
func deleteProjectItem(ctx context.Context, client *github.Client, owner, ownerType string, projectNumber int, itemID int64) (*mcp.CallToolResult, any, error) {
13451319
var resp *github.Response
13461320
var err error
@@ -1770,76 +1744,6 @@ func buildUpdateProjectItem(ctx context.Context, gqlClient *githubv4.Client, own
17701744
return &resolvedProjectItemUpdate{Project: payload}, nil
17711745
}
17721746

1773-
func buildIssueFieldUpdate(field *ResolvedField, raw any) (*IssueFieldCreateOrUpdateInput, error) {
1774-
if field == nil || field.IssueFieldNodeID == "" {
1775-
name := ""
1776-
if field != nil {
1777-
name = field.Name
1778-
}
1779-
return nil, ghErrors.NewStructuredResolutionError(
1780-
"issue_field_metadata_unavailable",
1781-
name,
1782-
"the attached Project field did not include the underlying Issue Field node ID; refresh field metadata and retry",
1783-
nil,
1784-
)
1785-
}
1786-
1787-
input := &IssueFieldCreateOrUpdateInput{FieldID: githubv4.ID(field.IssueFieldNodeID)}
1788-
if raw == nil {
1789-
deleteValue := githubv4.Boolean(true)
1790-
input.Delete = &deleteValue
1791-
return input, nil
1792-
}
1793-
1794-
invalidValue := func(hint string) (*IssueFieldCreateOrUpdateInput, error) {
1795-
return nil, ghErrors.NewStructuredResolutionError("invalid_field_value", field.Name, hint, nil)
1796-
}
1797-
1798-
switch field.DataType {
1799-
case "TEXT":
1800-
value, ok := raw.(string)
1801-
if !ok {
1802-
return invalidValue(fmt.Sprintf("Issue Field %q is TEXT; value must be a string or null to clear it", field.Name))
1803-
}
1804-
input.TextValue = githubv4.NewString(githubv4.String(value))
1805-
case "NUMBER":
1806-
value, ok := toFloat64(raw)
1807-
if !ok {
1808-
return invalidValue(fmt.Sprintf("Issue Field %q is NUMBER; value must be a finite number or null to clear it", field.Name))
1809-
}
1810-
number := githubv4.Float(value)
1811-
input.NumberValue = &number
1812-
case "DATE":
1813-
value, ok := raw.(string)
1814-
if !ok {
1815-
return invalidValue(fmt.Sprintf("Issue Field %q is DATE; value must be a YYYY-MM-DD string or null to clear it", field.Name))
1816-
}
1817-
if _, err := time.Parse("2006-01-02", value); err != nil {
1818-
return invalidValue(fmt.Sprintf("Issue Field %q is DATE; value %q must use YYYY-MM-DD format", field.Name, value))
1819-
}
1820-
input.DateValue = githubv4.NewString(githubv4.String(value))
1821-
case "SINGLE_SELECT":
1822-
value, ok := raw.(string)
1823-
if !ok || value == "" {
1824-
return invalidValue(fmt.Sprintf("Issue Field %q is SINGLE_SELECT; value must be a non-empty option name or ID, or null to clear it", field.Name))
1825-
}
1826-
optionID, err := resolveSingleSelectOptionByNameOrID(field, value)
1827-
if err != nil {
1828-
return nil, err
1829-
}
1830-
id := githubv4.ID(optionID)
1831-
input.SingleSelectOptionID = &id
1832-
default:
1833-
return nil, ghErrors.NewStructuredResolutionError(
1834-
"unsupported_field_type",
1835-
field.Name,
1836-
fmt.Sprintf("Issue Field %q has unsupported data type %q; supported types are TEXT, NUMBER, DATE, and SINGLE_SELECT", field.Name, field.DataType),
1837-
nil,
1838-
)
1839-
}
1840-
return input, nil
1841-
}
1842-
18431747
func extractPaginationOptionsFromArgs(args map[string]any) (github.ListProjectsPaginationOptions, error) {
18441748
perPage, err := OptionalIntParamWithDefault(args, "per_page", MaxProjectsPerPage)
18451749
if err != nil {

0 commit comments

Comments
 (0)