Skip to content

Commit baf940c

Browse files
committed
Expose update_project_items
Add the public projects_write contract, routing, handler coverage, and generated documentation for shared field updates across batches of up to 50 items. Co-authored-by: Lizeth Vera <47796851+veralizeth@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7ae767ff-c1d0-46a9-b126-2e91403993a0
1 parent 5bac322 commit baf940c

5 files changed

Lines changed: 1025 additions & 8 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1121,6 +1121,7 @@ The following sets of tools are available:
11211121
- `item_owner`: The owner (user or organization) of the repository containing the issue or pull request. Required for 'add_project_item' method. Also accepted by 'update_project_item' when resolving the item by issue number. (string, optional)
11221122
- `item_repo`: The name of the repository containing the issue or pull request. Required for 'add_project_item' method. Also accepted by 'update_project_item' when resolving the item by issue number. (string, optional)
11231123
- `item_type`: The item's type, either issue or pull_request. Required for 'add_project_item' method. (string, optional)
1124+
- `items`: The items to update with the top-level 'updated_field'. Required for 'update_project_items'; prefer it over calling 'update_project_item' in a loop. Each entry must match exactly one reference variant: 'node_id', numeric 'item_id', or 'item_owner' + 'item_repo' + 'issue_number'. Limit: 50 items per call. (object[], optional)
11241125
- `iteration_duration`: Duration in days for iterations of the field (e.g. 7 for weekly, 14 for bi-weekly). Required for 'create_iteration_field' method. (number, optional)
11251126
- `iterations`: Custom iterations for 'create_iteration_field' method. Only set this when you need iterations with varying durations, breaks between them, or specific titles. Otherwise omit it: GitHub auto-creates three iterations of 'iteration_duration' days starting on 'start_date', which is the right choice for most cases. (object[], optional)
11261127
- `method`: The method to execute (string, required)
@@ -1132,7 +1133,7 @@ The following sets of tools are available:
11321133
- `status`: The status of the project. Used for 'create_project_status_update' method. (string, optional)
11331134
- `target_date`: The target date of the status update in YYYY-MM-DD format. Used for 'create_project_status_update' method. (string, optional)
11341135
- `title`: The project title. Required for 'create_project' method. (string, optional)
1135-
- `updated_field`: Object describing the field to update and its new value. Required for 'update_project_item'. Two shapes are accepted: (1) by ID — {"id": 123456, "value": "..."}; (2) by name — {"name": "Status", "value": "In Progress"}. For single-select fields, option-name resolution requires the by-name shape; on the by-ID shape, pass the option ID. 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. For 'update_project_item' SINGLE_SELECT fields, the name form accepts option names; the ID form expects an option ID. (object, optional)
11361137

11371138
</details>
11381139

pkg/github/__toolsnaps__/projects_write.snap

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"readOnlyHint": false,
66
"title": "Manage GitHub Projects"
77
},
8-
"description": "Create and manage GitHub Projects: create projects, add/update/delete items, create status updates, and add iteration fields.",
8+
"description": "Create and manage GitHub Projects: create projects, add/update/delete items, bulk-update many items at once, create status updates, and add iteration fields.",
99
"inputSchema": {
1010
"properties": {
1111
"body": {
@@ -40,6 +40,64 @@
4040
],
4141
"type": "string"
4242
},
43+
"items": {
44+
"description": "The items to update with the top-level 'updated_field'. Required for 'update_project_items'; prefer it over calling 'update_project_item' in a loop. Each entry must match exactly one reference variant: 'node_id', numeric 'item_id', or 'item_owner' + 'item_repo' + 'issue_number'. Limit: 50 items per call.",
45+
"items": {
46+
"oneOf": [
47+
{
48+
"additionalProperties": false,
49+
"properties": {
50+
"node_id": {
51+
"description": "The project item's GraphQL node ID, as returned by 'list_project_items' or 'add_project_item'.",
52+
"type": "string"
53+
}
54+
},
55+
"required": [
56+
"node_id"
57+
],
58+
"type": "object"
59+
},
60+
{
61+
"additionalProperties": false,
62+
"properties": {
63+
"item_id": {
64+
"description": "The numeric project item ID.",
65+
"type": "integer"
66+
}
67+
},
68+
"required": [
69+
"item_id"
70+
],
71+
"type": "object"
72+
},
73+
{
74+
"additionalProperties": false,
75+
"properties": {
76+
"issue_number": {
77+
"description": "Issue number used to resolve the project item.",
78+
"type": "integer"
79+
},
80+
"item_owner": {
81+
"description": "Owner of the repository containing the issue.",
82+
"type": "string"
83+
},
84+
"item_repo": {
85+
"description": "Repository containing the issue.",
86+
"type": "string"
87+
}
88+
},
89+
"required": [
90+
"item_owner",
91+
"item_repo",
92+
"issue_number"
93+
],
94+
"type": "object"
95+
}
96+
],
97+
"type": "object"
98+
},
99+
"type": "array"
100+
},
43101
"iteration_duration": {
44102
"description": "Duration in days for iterations of the field (e.g. 7 for weekly, 14 for bi-weekly). Required for 'create_iteration_field' method.",
45103
"type": "number"
@@ -76,6 +134,7 @@
76134
"enum": [
77135
"add_project_item",
78136
"update_project_item",
137+
"update_project_items",
79138
"delete_project_item",
80139
"create_project_status_update",
81140
"create_project",
@@ -127,7 +186,43 @@
127186
"type": "string"
128187
},
129188
"updated_field": {
130-
"description": "Object describing the field to update and its new value. Required for 'update_project_item'. Two shapes are accepted: (1) by ID — {\"id\": 123456, \"value\": \"...\"}; (2) by name — {\"name\": \"Status\", \"value\": \"In Progress\"}. For single-select fields, option-name resolution requires the by-name shape; on the by-ID shape, pass the option ID. 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. For 'update_project_item' SINGLE_SELECT fields, the name form accepts option names; the ID form expects an option ID.",
190+
"oneOf": [
191+
{
192+
"additionalProperties": false,
193+
"properties": {
194+
"id": {
195+
"description": "The numeric project field ID.",
196+
"type": "integer"
197+
},
198+
"value": {
199+
"description": "The value to apply. Any JSON value is accepted; use null to clear the field."
200+
}
201+
},
202+
"required": [
203+
"id",
204+
"value"
205+
],
206+
"type": "object"
207+
},
208+
{
209+
"additionalProperties": false,
210+
"properties": {
211+
"name": {
212+
"description": "The project field name. Matching is case-insensitive.",
213+
"type": "string"
214+
},
215+
"value": {
216+
"description": "The value to apply. Any JSON value is accepted; use null to clear the field."
217+
}
218+
},
219+
"required": [
220+
"name",
221+
"value"
222+
],
223+
"type": "object"
224+
}
225+
],
131226
"type": "object"
132227
}
133228
},

pkg/github/projects.go

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const (
4545
projectsMethodGetProjectItem = "get_project_item"
4646
projectsMethodAddProjectItem = "add_project_item"
4747
projectsMethodUpdateProjectItem = "update_project_item"
48+
projectsMethodUpdateProjectItems = "update_project_items"
4849
projectsMethodDeleteProjectItem = "delete_project_item"
4950
projectsMethodListProjectStatusUpdates = "list_project_status_updates"
5051
projectsMethodGetProjectStatusUpdate = "get_project_status_update"
@@ -491,13 +492,90 @@ Use this tool to get details about individual projects, project fields, and proj
491492
return tool
492493
}
493494

495+
func updateProjectItemsItemSchema() *jsonschema.Schema {
496+
variant := func(required []string, properties map[string]*jsonschema.Schema) *jsonschema.Schema {
497+
return &jsonschema.Schema{
498+
Type: "object",
499+
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}},
500+
Properties: properties,
501+
Required: required,
502+
}
503+
}
504+
505+
return &jsonschema.Schema{
506+
Type: "object",
507+
OneOf: []*jsonschema.Schema{
508+
variant([]string{"node_id"}, map[string]*jsonschema.Schema{
509+
"node_id": {
510+
Type: "string",
511+
Description: "The project item's GraphQL node ID, as returned by 'list_project_items' or 'add_project_item'.",
512+
},
513+
}),
514+
variant([]string{"item_id"}, map[string]*jsonschema.Schema{
515+
"item_id": {
516+
Type: "integer",
517+
Description: "The numeric project item ID.",
518+
},
519+
}),
520+
variant([]string{"item_owner", "item_repo", "issue_number"}, map[string]*jsonschema.Schema{
521+
"item_owner": {
522+
Type: "string",
523+
Description: "Owner of the repository containing the issue.",
524+
},
525+
"item_repo": {
526+
Type: "string",
527+
Description: "Repository containing the issue.",
528+
},
529+
"issue_number": {
530+
Type: "integer",
531+
Description: "Issue number used to resolve the project item.",
532+
},
533+
}),
534+
},
535+
}
536+
}
537+
538+
func projectUpdatedFieldSchema() *jsonschema.Schema {
539+
value := &jsonschema.Schema{
540+
Description: "The value to apply. Any JSON value is accepted; use null to clear the field.",
541+
}
542+
variant := func(required []string, properties map[string]*jsonschema.Schema) *jsonschema.Schema {
543+
properties["value"] = value
544+
return &jsonschema.Schema{
545+
Type: "object",
546+
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}},
547+
Properties: properties,
548+
Required: required,
549+
}
550+
}
551+
552+
return &jsonschema.Schema{
553+
Type: "object",
554+
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. For 'update_project_item' SINGLE_SELECT fields, the name form accepts option names; the ID form expects an option ID.",
555+
OneOf: []*jsonschema.Schema{
556+
variant([]string{"id", "value"}, map[string]*jsonschema.Schema{
557+
"id": {
558+
Type: "integer",
559+
Description: "The numeric project field ID.",
560+
},
561+
}),
562+
variant([]string{"name", "value"}, map[string]*jsonschema.Schema{
563+
"name": {
564+
Type: "string",
565+
Description: "The project field name. Matching is case-insensitive.",
566+
},
567+
}),
568+
},
569+
}
570+
}
571+
494572
// ProjectsWrite returns the tool and handler for modifying GitHub Projects resources.
495573
func ProjectsWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
496574
tool := NewTool(
497575
ToolsetMetadataProjects,
498576
mcp.Tool{
499577
Name: "projects_write",
500-
Description: t("TOOL_PROJECTS_WRITE_DESCRIPTION", "Create and manage GitHub Projects: create projects, add/update/delete items, create status updates, and add iteration fields."),
578+
Description: t("TOOL_PROJECTS_WRITE_DESCRIPTION", "Create and manage GitHub Projects: create projects, add/update/delete items, bulk-update many items at once, create status updates, and add iteration fields."),
501579
Annotations: &mcp.ToolAnnotations{
502580
Title: t("TOOL_PROJECTS_WRITE_USER_TITLE", "Manage GitHub Projects"),
503581
ReadOnlyHint: false,
@@ -512,6 +590,7 @@ func ProjectsWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
512590
Enum: []any{
513591
projectsMethodAddProjectItem,
514592
projectsMethodUpdateProjectItem,
593+
projectsMethodUpdateProjectItems,
515594
projectsMethodDeleteProjectItem,
516595
projectsMethodCreateProjectStatusUpdate,
517596
projectsMethodCreateProject,
@@ -560,9 +639,11 @@ func ProjectsWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
560639
Type: "number",
561640
Description: "The pull request number (use when item_type is 'pull_request' for 'add_project_item' method). Provide either issue_number or pull_request_number.",
562641
},
563-
"updated_field": {
564-
Type: "object",
565-
Description: "Object describing the field to update and its new value. Required for 'update_project_item'. Two shapes are accepted: (1) by ID — {\"id\": 123456, \"value\": \"...\"}; (2) by name — {\"name\": \"Status\", \"value\": \"In Progress\"}. For single-select fields, option-name resolution requires the by-name shape; on the by-ID shape, pass the option ID. Set value to null to clear the field.",
642+
"updated_field": projectUpdatedFieldSchema(),
643+
"items": {
644+
Type: "array",
645+
Description: "The items to update with the top-level 'updated_field'. Required for 'update_project_items'; prefer it over calling 'update_project_item' in a loop. Each entry must match exactly one reference variant: 'node_id', numeric 'item_id', or 'item_owner' + 'item_repo' + 'issue_number'. Limit: " + strconv.Itoa(maxProjectItemsPerBatch) + " items per call.",
646+
Items: updateProjectItemsItemSchema(),
566647
},
567648
"body": {
568649
Type: "string",
@@ -723,6 +804,8 @@ func ProjectsWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
723804
return utils.NewToolResultError("updated_field must be an object"), nil, nil
724805
}
725806
return updateProjectItem(ctx, client, gqlClient, owner, ownerType, projectNumber, itemID, fieldValue)
807+
case projectsMethodUpdateProjectItems:
808+
return updateProjectItemsBatch(ctx, client, gqlClient, owner, ownerType, projectNumber, args)
726809
case projectsMethodDeleteProjectItem:
727810
itemID, err := RequiredBigInt(args, "item_id")
728811
if err != nil {

0 commit comments

Comments
 (0)