Appearance
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 / recipes — AUTH="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.