feat(mcp): add unclaim_issue, queue, issue-list, pr-fix, and groomer tools#644
Conversation
The unclaim operation existed only as the HTTP route (/api/issues/unclaim); the MCP server exposed claim/status/sync but no release, so unclaiming from an agent session meant raw gh label surgery that misses the lease release and the in-progress -> ready transition. Add unclaimIssue to mc-client (resolve -> POST /api/issues/unclaim, mirroring claimIssue) and register unclaim_issue with the same DISPATCH_AGENT_NAME fallback contract as claim_issue. The route already owns the semantics: agent label off, lease released (AgentWork on the operator path), status/in-progress -> status/ready, lane preserved, audit row written. AI assistance: authored with Claude Code. Signed-off-by: Jory Irving <jory.irving@users.noreply.github.com>
Round out the MCP surface with the read/act verbs agents currently reach via raw curl per the workspace contracts: - get_queue: the agent's ranked queue (pr-fix first), lane-filterable, DISPATCH_AGENT_NAME fallback like claim_issue - list_issues: filtered issue queries (repo/status/lane/agent/priority) - list_pr_fixes + mark_pr_fix: the PR-fix worker contract (list queued items, mark fixed/blocked/stale/ignored) - run_groomer: trigger a hosted groomer run now (optionally targeting a specific issue) instead of waiting for the cron All thin wrappers over existing authorized API routes; no route changes. AI assistance: authored with Claude Code. Signed-off-by: Jory Irving <jory.irving@users.noreply.github.com>
There was a problem hiding this comment.
AI Automated Review
Full PR review.
Analysis engine: MiniMax-M2.7@https://litellm.jory.dev/v1 (anthropic) — escalated (fast_low_confidence)
Recommendation: Approve
This PR adds six new MCP tools (unclaim_issue, get_queue, list_issues, list_pr_fixes, mark_pr_fix, run_groomer) as thin wrappers over existing Dispatch API routes. The implementation is consistent with established codebase patterns and passes all CI checks.
Change-by-Change Findings
src/lib/mc-client.ts (+101 lines)
Adds the following client functions:
unclaimIssue— resolves the issue CUID first, then POSTs to/api/issues/unclaimwith the same body shape asclaimIssue(issueId, repoFullName, issueNumber, agentName). ReturnsUnclaimIssueResult { success, labels }.getQueue— GET to/api/agents/{agentName}/queuewith optionallane,exclude_decomposed,includeClaimed,includeRenovatequery params.listIssues— GET to/api/issueswith optionalrepo,status,lane,agent,priority,includeClosedquery params.listPrFixes— GET to/api/pr-fix-queue/queuedwith optionallane,include_blockedquery params.markPrFix— POST to/api/pr-fix-queue/markwith{ repo, pr, status, note }.runGroomer— POST to/api/groomer/runwith optional{ repoFullName, issueNumber }.
Observations:
includeClosedstays as camelCase when building the query string (line 277), whileincludeBlocked → include_blockedis snake_cased (line 291). This matches the underlying API routes (/api/issuesvs/api/pr-fix-queue/queued) and is internally consistent.- Return types use
unknown[]for list operations. This is looser than typed interfaces likeUnclaimIssueResult, but consistent with the PR scope of thin API wrappers, and matches the existinggetClaimableLanespattern in this module.
src/mcp/server.ts (+203 lines)
Adds six MCP tool handlers and registers them via server.registerTool():
unclaimIssueHandler— validates agentName viaresolveAgentName, then delegates tounclaimIssue. Error message follows the established "Do not use generic identities like 'Dispatch MCP'" convention.getQueueHandler— sameresolveAgentNamevalidation and error pattern.listIssuesHandler— no auth/env dependency; passes filter args directly.listPrFixesHandler— no auth/env dependency.markPrFixHandler— validates requiredrepo,pr,statusvia Zod schema;noteis optional.runGroomerHandler— optional targeting via Zod schema; POSTs empty body when no target given.
Observations:
- The
agentNamevalidation pattern (require explicit arg or fall back toDISPATCH_AGENT_NAME) is correctly applied tounclaimIssueHandlerandgetQueueHandler. This is consistent withclaimIssueHandler. - The
"Do not use generic identities like 'Dispatch MCP'"error suffix is present in both new handlers and matches the existing convention. markPrFixHandleracceptsstatus: stringwithout pre-validating againstfixed|blocked|stale|ignored; the API validates this. This matches the pattern used bysetIssueStatuswhich also acceptsstatus: stringand relies on API-level validation.
src/lib/mc-client.test.ts (+109 lines)
unclaimIssue: mocks resolve → unclaim fetch chain; asserts POST body; tests "not found" rejection.getQueue: asserts URL contains/api/agents/test-agent/queueand query params.listIssues: asserts URL building withrepo,status,laneparams.listPrFixes: asserts URL withlaneandinclude_blockedparams.markPrFix: asserts POST body with all fields including optionalnote.runGroomer: asserts empty body default and targeted body variant.
src/mcp/server.test.ts (+155 lines)
unclaimIssueHandler: mocks resolve → unclaim; checksisErroris undefined on success; assertsstatus/readyin returned labels; tests "agentName required" error; tests API rejection surfaces asisError: true.getQueueHandler: tests env var fallback (DISPATCH_AGENT_NAME); tests error when both arg and env are missing.listIssuesHandler: tests JSON return path.listPrFixesHandler: testsincludeBlockedpass-through.markPrFixHandler: tests 404 error surfaces asisError: true.runGroomerHandler: testscandidateNumberin response.
Standards Compliance
The PR conforms to the following repository conventions:
| Convention | Status |
|---|---|
DISPATCH_AGENT_NAME fallback for agent identity (AGENTS.md §Agent Workflow Contract) |
✅ Consistent with claimIssueHandler |
| No generic agent names in error messages | ✅ Both new handlers include the "Do not use generic identities like 'Dispatch MCP'" message |
| Thin wrappers over existing authorized API routes | ✅ Confirmed — no new routes added |
Bearer auth (DISPATCH_AGENT_TOKEN) for agent API calls |
✅ All functions use mcJson which sets bearer auth header |
| Audit trail for mutation operations | ✅ unclaimIssue hits /api/issues/unclaim (already audited), markPrFix hits /api/pr-fix-queue/mark (already audited) |
Label pattern agent/* for ownership |
✅ Tool descriptions reference agent label semantics correctly |
| Test coverage for error paths | ✅ Covers: missing agentName, API rejection, not-found, env fallback |
Tool Harness Findings
The gh_api call for repos/misospace/miso-gallery/compare/... returned 404 — this is unrelated to the PR (the repo misospace/miso-gallery is not involved in this change). All other tool harness calls succeeded.
Unknowns / Needs Verification
None. The implementation is fully visible in the diff, tests are comprehensive, CI passes, and the PR description provides clear motivation traced to real-world context (the 17-issue strand from 2026-07-21, referenced in foreman-dispatch-bridge#46). No blocking gaps remain.
Summary
unclaim_issue— exposes/api/issues/unclaim: agent label off, lease released (AgentWork on operator path),in-progress→ready, lane preserved, audit row. SameDISPATCH_AGENT_NAMEfallback contract asclaim_issue.get_queue— the agent's ranked queue (PR-fix items first), lane-filterable.list_issues— filtered issue queries (repo/status/lane/agent/priority, optional closed).list_pr_fixes+mark_pr_fix— the PR-fix worker contract (list queued items; markfixed/blocked/stale/ignoredwith note).run_groomer— trigger a hosted groomer run immediately, optionally targeting one issue.All thin wrappers over existing authorized API routes — no route changes. Together with the existing six tools this covers the full agent workspace contract, so agent docs no longer need raw-curl instructions.
Why
Unclaiming from an agent session previously required raw label edits that miss the lease release and status transition (see the 17-issue strand reconciled by hand 2026-07-21; context foreman-dispatch-bridge#46). The queue/pr-fix/groomer verbs are what agent workspace docs currently teach via curl.
Verification
vitest run: full suite green, incl. new handler + client tests (URL/body assertions, env-fallback, error paths).tsc --noEmit+eslintclean.