Skip to content

date: 2026-06-20 tags: [github, projects-v2, graphql, mcp, tooling, web-env] status: active graduated_to:

Projects v2 Status & native issue dependencies — set them with curl + the env token, not the MCP tools

Symptom — Filed an Epic/Sprint/Task tree from to-issues and reported that the board Status and native blocked-by links "couldn't be set with the tools available." That was wrong — both are fully scriptable.

Root cause — The GitHub MCP tools (mcp__github__*) cover issues, sub-issues, labels and the org issue-fields, but they do not expose Projects v2 board fields (Status/board order) or the issue dependencies API. It's easy to conclude those are impossible. They aren't: the web env ships an authenticated GH_TOKEN, and curl against api.github.com reaches the REST + GraphQL APIs directly. The token can look bogus (≈8 chars — the proxy resolves it), but curl -H "Authorization: Bearer $GH_TOKEN" https://api.github.com/user returns 200. Verify auth that way before assuming a capability gap.

Fix / recipesAUTH="Authorization: Bearer $GH_TOKEN". IDs below are repo-specific — re-discover them, don't hardcode.

1. Native blocked-by dependencies (REST)

The body takes the blocking issue's REST id (not its number — the id is returned by issue_write/GET /issues/{n}).

bash
curl -s -X POST -H "$AUTH" -H "Accept: application/vnd.github+json" \
  https://api.github.com/repos/Tomat-Labs/Tempo/issues/<N>/dependencies/blocked_by \
  -d '{"issue_id":<BLOCKER_REST_ID>}'              # 201 on success
# read back: GET …/issues/<N>/dependencies/blocked_by  -> [{number,…}]
# remove:    DELETE …/issues/<N>/dependencies/blocked_by/<BLOCKER_REST_ID>

2. Projects v2 board Status (GraphQL)

Discover the project id, the Status field id, and its option ids once:

bash
gql(){ curl -s -H "$AUTH" -H "Content-Type: application/json" \
  https://api.github.com/graphql -d "$(jq -n --arg q "$1" '{query:$q}')"; }

gql 'query{organization(login:"Tomat-Labs"){projectV2(number:3){
  id field(name:"Status"){... on ProjectV2SingleSelectField{id options{id name}}}}}}'

Tempo's board (Projects v2 #3): project PVT_kwDOEVOzGc4BZyyo, Status field PVTSSF_lADOEVOzGc4BZyyozhUvAaU, options — Needs Triage Needs Info Ready for Agent Ready for Human In Progress Done Wontfix.

Then per issue: add it to the board (idempotent — returns the existing item if already added), then set the field. The content id is the issue's GraphQL node id (repository.issue(number:N){id}), not the REST id.

bash
ITEM=$(gql "mutation{addProjectV2ItemById(input:{projectId:\"$PROJ\",contentId:\"$NODE_ID\"}){item{id}}}" \
       | jq -r '.data.addProjectV2ItemById.item.id')
gql "mutation{updateProjectV2ItemFieldValue(input:{projectId:\"$PROJ\",itemId:\"$ITEM\",fieldId:\"$FIELD\",value:{singleSelectOptionId:\"$OPT\"}}){projectV2Item{id}}}"

3. Org issue-fields (Priority / Effort / Area / dates)

These — unlike Status — are settable through the MCP tool: mcp__github__issue_write with issue_fields:[{field_name:"Priority",field_option_name:"High"}]. Enumerate valid fields/options with mcp__github__list_issue_fields. (Status is absent from that list precisely because it's a Projects v2 board field, not an org issue-field — that's the tell for which API you need.)

Guard — none automatic. The habit: when an MCP GitHub tool seems to lack a capability, probe GET /user with the token first; if it authenticates, reach for REST/GraphQL via curl before declaring it impossible.