Optimized wrapper around GitHub APIs for working with Projects v2 boards. Designed to be agent-friendly — trimmed responses, human-readable field names for mutations, server-side filtering, and direct-to-disk output via curl.
This project exists only to fill gaps in GitHub's own tooling. Any functionality that can be replaced by a GitHub-supported tool (gh CLI, GitHub REST/GraphQL APIs, GitHub's official MCP server) should be replaced. Before adding new capabilities, check whether GitHub has shipped native support. Before keeping existing capabilities, periodically verify they're still necessary.
Specific gaps filled (as of 2026-05):
gh project item-listhas no server-side filtering. On a 3,000+ item board, fetching everything and filtering client-side isn't workable.- All GitHub mutation tools require raw node IDs.
gh project item-edit, GitHub's MCPprojects_write, and raw GraphQL all needPVTI_,PVTSSF_, and option IDs — not human-readable names like "Status" and "In Progress." - No GitHub tool supports batch field mutations. Setting the same field on 15 items means 15 sequential calls, each re-resolving IDs.
If GitHub adds filtering to gh project item-list, name-based mutations, or batch operations, the corresponding code here should be retired.
An agent doing board maintenance needs two categories of operations: board field operations (Status, Cycle Theme, Dev Days Estimate — project-level fields) and issue/PR operations (review state, assignees, milestones, labels — repository-level properties). No single tool covers both well.
- Board field reads (list items, filter, get field values): This project — server-side filtering with trimmed responses, direct to disk via curl
- Board field mutations (set Status, Cycle Theme, etc.): This project — only tool with name-based mutations and bulk support
- Issue/PR metadata reads (review state, draft, labels):
ghCLI - Issue/PR mutations (assignees, milestones, reviewers):
ghCLI - Schema discovery (what fields exist, what options are valid):
gh project field-listworks fine — no unique value from this project
These are the operations specific to GitHub Projects v2 boards — reading and writing the custom fields that live on the project, not on the issue/PR itself.
| Capability | gh CLI |
GitHub REST | GitHub GraphQL | GitHub MCP | This project |
|---|---|---|---|---|---|
| List board items | gh project item-list |
GET /projectsV2/{n}/items |
projectV2.items query |
list_project_items |
GET /items |
| Server-side filtering | No --query flag; must fetch all items |
q= param with filter syntax |
Manual query construction | query param |
query param (wraps REST) |
| OR filter syntax | No | No | No | No | (status:"In Progress") OR (status:"Review") — expanded to multiple queries, deduplicated |
| Response size per item | ~800 bytes (includes body text) | ~8KB (full PR/issue objects embedded) | You pick fields, but must craft query | Verbose (field values wrapped in {html, raw}). Upstream tracking issue:github/github-mcp-server#2383 |
~200-300 bytes (trimmed to field values only) |
| Field value trimming | No (returns full PR/issue body text) | No (~8KB linked PR objects) | You craft the query | No (full objects in response) | All fields trimmed to display values: single-select/text/iteration → display name; milestone → title; sub-issues → N/N; linked PRs → {repo, number, state, title, author} (~100 bytes vs ~8KB); assignees/reviewers → comma-separated logins |
| Direct to disk (bypass LLM context) | Yes (pipe stdout) | Yes (curl) | Yes (curl) | No (MCP responses enter context — #2383) | Yes (curl) |
| Set a field value (single or bulk) | item-edit — requires --field-id, --single-select-option-id, --project-id (all raw node IDs) |
No mutation support | updateProjectV2ItemFieldValue — requires project/field/option node IDs (3-4 lookups) |
update_project_item — requires numeric field ID |
PUT /items/field/{name} — human-readable names, single or bulk, batches up to 25 per request |
| Item lookup by reference | No (need PVTI_ node ID) | No (need to query + filter) | No (need node ID) | No (need item ID) | GET /items/dealbot%23458 — parses repo#number, owner/repo#number, or URL |
Item updated_at and creator |
No | Available in raw response but not surfaced | Queryable but manual | No | Not yet — see future ideas |
| Discover field options | gh project field-list (clean) |
GET /projectsV2/{n}/fields |
Inline fragment query | list_project_fields |
GET /fields/{name}/options |
| Audit logging | No | No | No | No | Append-only JSONL with caller, old/new values |
GitHub's official MCP server has a Projects v2 toolset (projects_list, projects_get, projects_write) available at the /x/projects endpoint. It supports query filtering, pagination, field discovery (including single-select options), and mutations.
The non-negotiable blocker: context window bloat. Each project item response from GitHub's MCP is ~8KB because it includes the full issue/PR body, complete repository object (~2KB of URL templates), and full user objects for every author/assignee/milestone-creator. The fields parameter controls which project fields are returned but there is no way to suppress the content blob.
| Query size | GitHub MCP payload | Token cost | Impact |
|---|---|---|---|
| 10 items | ~80KB | ~20K tokens | Noticeable |
| 50 items (max per_page) | ~400KB | ~100K tokens | Half the context window |
| 100 items (2 pages) | ~800KB | ~200K+ tokens | Entire context window consumed |
This project returns ~200-300 bytes per item (just the project field values) — a ~40x reduction. With the REST API, data goes directly to disk via curl and never enters LLM context at all.
| GitHub Projects MCP | This project | |
|---|---|---|
| Per-item response size | ~8KB | ~200-300 bytes |
| 50-item query | ~400KB / ~100K tokens | ~10-15KB / ~3-4K tokens |
| Field name resolution | Raw IDs required | By name ("Status" → "Done") |
| Filter syntax docs | None in tool description | Comprehensive reference in MCP coordinator |
| Mutation UX | 3 tool calls with raw IDs | 1 curl call: PUT /items/field/Status with {"item_refs": ["dealbot#458"], "value": "..."} |
| Bulk mutations | No | Up to 25 per request |
| Audit logging | None | JSONL with old/new values |
This is a known problem across the GitHub MCP server (20+ comments, open since April 2025). The maintainers have been fixing it tool-by-tool using "minimal types", but the projects tools haven't been optimized yet. We filed github/github-mcp-server#2383 requesting compact output for project items.
If #2383 gets addressed, we should revisit this decision — GitHub's official tooling could replace this project's read path. For the full evaluation, see FilOzone/tpm-utils#25 (comment).
These operations are not covered by this project and should use GitHub's own tools:
| Capability | Best tool | Example |
|---|---|---|
| PR review state (approved, changes requested) | gh pr view --json reviewDecision,reviews |
Check if PR has approval before status transition |
| PR draft status | gh pr list --json isDraft |
Identify draft PRs for triage rules |
| Assignee mutations | gh issue edit --add-assignee / gh pr edit --add-assignee |
Set PR author as assignee |
| Milestone mutations | gh issue edit --milestone |
Assign milestones |
| Label operations | gh issue edit --add-label |
Add/remove labels |
| Issue/PR creation | gh issue create / gh pr create |
Create new items |
| Cross-repo search | gh search prs --repo |
Find PRs across repos |
| Review requests | gh pr edit --add-reviewer |
Request reviews |
All functions take session, org, and project_number as explicit arguments — no hardcoded defaults or environment variables.
import requests
from github_projects_client import list_items, get_item, set_field_value
session = requests.Session()
session.headers["Authorization"] = f"Bearer {token}"
session.headers["Content-Type"] = "application/json"
# List non-Done PRs
result = list_items(session, org="MyOrg", project_number=1, query='is:pr -status:"Done"')
for item in result["items"]:
print(item["Title"], item["Status"])
# Look up a specific item
detail = get_item(session, org="MyOrg", project_number=1, item_ref="my-repo#42")
# Set a field by name
set_field_value(session, org="MyOrg", project_number=1,
item_ref="my-repo#42", field_name="Status", value="⌨️ In Progress")Public API:
| Function | Module | Description |
|---|---|---|
list_items |
items |
List project items with filter query and pagination |
get_item |
items |
Look up a single item by repo#number, owner/repo#number, or URL |
list_fields |
items |
List all project field names and REST numeric IDs |
list_field_options |
fields |
Enumerate options for single-select and iteration fields |
resolve_view_url |
views |
Parse a project view URL into filter, fields, and group-by metadata |
set_field_value |
mutations |
Set a project field by name (resolves field/option IDs internally) |
set_field_value_bulk |
mutations |
Set a field on multiple items in batched GraphQL mutations |
expand_or_query |
query |
Expand (branch1) OR (branch2) syntax into individual queries |
graphql_query |
api |
Low-level GraphQL query helper |
list_field_ids_by_name |
api |
REST field name → numeric ID mapping |
fetch_items_rest |
api |
Low-level REST item fetcher with pagination |
A REST API wrapping the Python library, powered by FastAPI. Designed for agents that work via curl — data goes directly to disk without entering LLM context.
cd github-projects-client
uv run github-projects-apiServer starts on http://127.0.0.1:8080. Override with HOST and PORT environment variables.
Once running, visit http://localhost:8080/docs for the interactive Swagger UI, or fetch the OpenAPI spec at http://localhost:8080/openapi.json.
All endpoints require a GitHub PAT as a bearer token:
# List non-Done PRs → disk, never enters LLM context
curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
"http://localhost:8080/orgs/FilOzone/projects/14/items?query=is:pr+-status:%22🎉+Done%22" \
> board_prs.json
# Set a field (single or bulk)
curl -s -X PUT -H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Content-Type: application/json" \
-d '{"item_refs": ["dealbot#458"], "value": "🎉 Done"}' \
"http://localhost:8080/orgs/FilOzone/projects/14/items/field/Status"Endpoints:
| Method | Path | Description |
|---|---|---|
| GET | /orgs/{org}/projects/{n}/items |
List board items with filtering and pagination |
| GET | /orgs/{org}/projects/{n}/items/{item_ref} |
Get a single board item |
| GET | /orgs/{org}/projects/{n}/items/view |
List items from a saved view URL |
| GET | /orgs/{org}/projects/{n}/fields |
List all board fields |
| GET | /orgs/{org}/projects/{n}/fields/{name}/options |
List field options |
| PUT | /orgs/{org}/projects/{n}/items/field/{name} |
Set a field (single or bulk) |
| GET | /orgs/{org}/projects/{n}/audit-log |
Read audit log entries |
cd github-projects-client
# Unit tests (no GitHub API calls)
uv run pytest tests/ -v -m "not integration"
# Integration tests (requires GITHUB_TOKEN)
GITHUB_TOKEN=$(gh auth token) uv run pytest tests/ -vSee foc-board-rules/future-ideas.md for the central list. Relevant items:
list_itemsdoes not surface built-in item properties likeupdated_atandcreator. See future ideas.- Remove
format=compact— context-window optimization that doesn't apply when data goes to disk - Remove
GET /fields/{name}/options—gh project field-listcovers this natively
- specs/003-generalize-mcp-client/contracts/shared-client-api.md — original client library API
- specs/005-rest-api-layer/ — REST API server spec, plan, and contracts