Skip to content

Add update_project_items batch/bulk field-value writes via aliased GraphQL mutations#2903

Draft
veralizeth wants to merge 2 commits into
zwick-paginate-project-item-lookupfrom
veralizeth/mcp-batch-bulk-field-value
Draft

Add update_project_items batch/bulk field-value writes via aliased GraphQL mutations#2903
veralizeth wants to merge 2 commits into
zwick-paginate-project-item-lookupfrom
veralizeth/mcp-batch-bulk-field-value

Conversation

@veralizeth

@veralizeth veralizeth commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Summary

update_project_items now writes via chunked, aliased updateProjectV2ItemFieldValue / clearProjectV2ItemFieldValue GraphQL mutations (built at runtime with reflect.StructOf) instead of one REST PATCH per item.

Why

Fixes https://github.com/github/planning-tracking/issues/3331 (MCP half only; the CLI half will follow in a separate PR).

Stacking note: this PR is stacked on #2914 (zwick-paginate-project-item-lookup), which extended the issue-number project-item resolver to paginate. This PR's base branch is set to that branch so the diff here only shows the batch-write changes.

What changed

  • Extended ResolvedField / projectFieldsCache to also retain the GraphQL field node ID and to support lookup by numeric field ID (in addition to name), amortized once per batch.
  • Added node_id as a third item-reference form (alongside item_id and item_owner+item_repo+issue_number), bypassing REST lookup entirely; each item must supply exactly one form.
  • Numeric item_id values are deduplicated and resolved to project-item node IDs via bounded-concurrency (5 in flight) REST GETs; the Paginate project item lookup across memberships #2914 paginated issue resolver now also returns the item's node ID.
  • updated_field.value is converted to the correct ProjectV2FieldValue member for TEXT, NUMBER, DATE, SINGLE_SELECT, and ITERATION; other data types return a deterministic per-item error directing callers to update_project_item. value: null now routes to the clear mutation instead of sending an empty update.
  • Duplicate item+field targets within one call are rejected before any writes, so partitioning update/clear work can't leave the final state ambiguous.
  • New pkg/github/projects_batch_mutation.go isolates the reflection: a struct type cached by (kind, chunk size) with positional fields (Item0..ItemN-1), positional GraphQL aliases (item0..), and positional input variables. Per the pinned githubv4.Client.Mutate signature, alias 0's input is always bound to $input; aliases 1+ use $input1, $input2, ... via the variables map (the "first-input workaround"). No owner/id/name/value data is ever embedded in a struct tag.
  • Update and clear mutations are executed in separate, sequential chunks of 20 (tool-level cap stays 100 items/call). Per-item results are tri-state (succeeded / failed / unknown): a populated alias is a confirmed success even if the overall response also carried a GraphQL error; an unconfirmed alias after an error is unknown (the pinned client drops errors[].path, so we can't attribute an error to a specific alias). After an ambiguous/transport-level chunk failure or context cancellation, no further chunks are sent — we don't retry or fall back to per-item mutations.
  • Removed the stale comment claiming the pinned client couldn't build dynamic aliases (see Query batching and/or dynamic queries. shurcooL/githubv4#17, Use Raw repo resources #70, PR chore: go releaser #71, assert request params and body in pull_request_test #95).

MCP impact

  • Tool schema or behavior changed
    update_project_items gains node_id as an item-reference option; its response now includes tri-state status and unknown counts alongside succeeded/failed.

Prompts tested (tool changes only)

  • "In octo-org project 1, set the Status field to Done for items 1001 and 1002"
  • "Move issue octo/repo#123 to In Progress in project 1, and clear its Estimate field"
  • "Update 25 project items' Sprint field in one call" (exercises the 20-item chunk boundary)

Security / limits

  • Data exposure, filtering, or token/size limits considered
    Tool-level cap stays at 100 items/call; the internal wire chunk size of 20 is provisional (not benchmarked against the live API) and chosen to leave headroom under typical ~10s gateway/client timeouts for a single aliased-mutation round trip.

Tool renaming

  • I am renaming tools as part of this PR (e.g. a part of a consolidation effort)
    • I have added the new tool aliases in deprecated_tool_aliases.go
  • I am not renaming tools as part of this PR

Lint & tests

  • Linted locally with ./script/lint
  • Tested locally with ./script/test

Docs

  • Updated (README / docs / examples)

@Swastiksrijan

Copy link
Copy Markdown

go

@zwick zwick self-assigned this Jul 20, 2026
@zwick
zwick force-pushed the veralizeth/mcp-batch-bulk-field-value branch from 15ca2ee to b8f5ca9 Compare July 20, 2026 19:43
@zwick
zwick changed the base branch from main to zwick-paginate-project-item-lookup July 20, 2026 19:43
…utations

Rewrite update_project_items to resolve the project, every distinct
field, and every item once up front, then write via chunked, aliased
updateProjectV2ItemFieldValue / clearProjectV2ItemFieldValue GraphQL
mutations built at runtime with reflect.StructOf, instead of one REST
PATCH per item.

- Extend ResolvedField/projectFieldsCache with GraphQL node IDs and
  numeric-ID lookup, alongside existing name-based lookup.
- Add node_id as a third item-reference form (alongside item_id and
  item_owner/item_repo/issue_number), bypassing REST lookup entirely.
- Deduplicate numeric item_id lookups with bounded-concurrency REST
  GETs; extend the #2914 paginated issue resolver to also return the
  item's node ID.
- Convert updated_field.value to the correct ProjectV2FieldValue member
  for TEXT, NUMBER, DATE, SINGLE_SELECT, and ITERATION; route null
  values to the clear mutation instead of an empty update.
- Reject duplicate item+field targets before any writes.
- Isolate the reflection mechanics in projects_batch_mutation.go: a
  cached-by-(kind,size) reflected struct type with positional aliases
  (item0..itemN-1) and positional input variables (, ,
  ...), matching the first-input workaround required by the pinned
  githubv4.Client.Mutate signature.
- Execute updates then clears in sequential chunks of 20 (provisional,
  documented headroom rationale), tracking tri-state per-item results
  (succeeded/failed/unknown); abort remaining chunks after an ambiguous
  transport-level failure or context cancellation without retrying.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d7dc302d-e6f2-41e9-a2c8-ed598de47067
@zwick zwick changed the title Adding an initial draft of batch/bulk field-value writes Adding an initial draft of batch/bulk field-value writes Jul 20, 2026
@zwick zwick changed the title Adding an initial draft of batch/bulk field-value writes Add update_project_items batch/bulk field-value writes via aliased GraphQL mutations Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants