Context
While triaging #13260, I attempted to use the GitHub Workflow MCP manage_issue_labels tool to add ai to an ordinary GitHub Issue. The tool failed before mutation:
Tool Error: GraphQL API request failed. Message: GitHub API error: Could not resolve to a PullRequest with the number of 13260.
I verified the authenticated GitHub CLI account as neo-gpt before the public write path, then used REST as a manual fallback to apply the label. The MCP tool failure is still reproducible against the same issue number.
Release classification: not release-blocking; agent-ops hardening. The REST fallback exists, but the MCP surface should not fail its primary issue-label use case.
Live latest-open sweep: checked the latest 20 open issues on 2026-06-14T20:21Z; no equivalent open issue found. A2A in-flight sweep: checked latest 30 messages; no active claim/intent for this scope found.
The Problem
manage_issue_labels is documented as a unified issue-or-pull-request label tool. The merged support from #10077 / PR #11696 covered the old PR-label failure class, but the current live failure is the mirror image: an issue-only number fails because GitHub GraphQL rejects the pullRequest(number:) field when no PR with that repository number exists.
The current unit tests mock the dual lookup as if GitHub returns { issue, pullRequest: null } or { issue: null, pullRequest }. Live GraphQL can instead fail the whole operation when one branch of the combined query cannot resolve, so the fallback expression never executes.
The Architectural Reality
Current origin/dev shape:
ai/mcp/server/github-workflow/toolService.mjs maps manage_issue_labels to IssueService.manageIssueLabels().
ai/services/github-workflow/IssueService.mjs#manageIssueLabels() delegates add/remove to addLabels() / removeLabels().
IssueService.#getIds() calls one combined GET_ISSUE_AND_LABEL_IDS query, then chooses data.repository.issue?.id || data.repository.pullRequest?.id.
ai/services/github-workflow/queries/issueQueries.mjs defines GET_ISSUE_AND_LABEL_IDS with both issue(number: $issueNumber) and pullRequest(number: $issueNumber) in the same query.
test/playwright/unit/ai/services/github-workflow/IssueService.spec.mjs has contract coverage for the intended dual behavior, but not for the live GraphQL partial-field rejection path.
The Fix
Make manage_issue_labels resolve the labelable target without relying on a single combined issue+PR query that can fail on the missing side.
Likely shape:
- Fetch the issue labelable ID first using an issue-only query and the repository label IDs.
- If the issue lookup reports the target does not exist, fetch the PR labelable ID using a PR-only query.
- Preserve successful issue labels, successful PR labels, missing-label errors, and missing-target structured errors.
- Add a unit test that simulates the live issue-number failure class: the PR lookup path must not run or must not poison an issue target when the issue exists.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
Docs |
Evidence |
manage_issue_labels MCP tool |
Existing OpenAPI tool contract + this ticket |
Add/remove labels for either an Issue or Pull Request number. |
If neither target exists, return the existing structured GraphQL error; if labels are missing, return the existing missing-label error. |
No OpenAPI shape change. |
Unit tests for issue success, PR success, missing target, missing label, and live-style partial lookup failure. |
IssueService.#getIds() labelable resolution |
IssueService.manageIssueLabels() implementation |
Resolve the correct Labelable ID without a combined issue+PR query that can be rejected by GitHub for one missing side. |
Issue lookup failure should only fall through to PR lookup when it means target missing; unrelated GraphQL/auth/rate errors should still surface. |
JSDoc or inline summary if helper split is non-obvious. |
Focused IssueService.spec.mjs tests with mocked GraphqlService.query() call order and error shapes. |
Decision Record impact
None. This is a bug fix in an existing MCP service surface and does not amend an ADR.
Acceptance Criteria
manage_issue_labels succeeds for a real issue number even when no PR with the same number exists.
manage_issue_labels still succeeds for a PR number.
- Missing target and missing label cases continue to return structured errors.
- Unit coverage includes the live failure class that produced
Could not resolve to a PullRequest with the number of 13260.
- No OpenAPI request/response schema change is introduced.
Out of Scope
- Creating a separate PR-only label tool.
- Reworking label validation or pagination beyond what is needed for the existing
maxRepoLabels contract.
- Changing issue/PR comment, assignment, review, or project membership tools.
Related
Related: #10077, #11696
Retrieval Hint: manage_issue_labels issue-only number PullRequest resolution GraphQL partial field rejection
Context
While triaging
#13260, I attempted to use the GitHub Workflow MCPmanage_issue_labelstool to addaito an ordinary GitHub Issue. The tool failed before mutation:I verified the authenticated GitHub CLI account as
neo-gptbefore the public write path, then used REST as a manual fallback to apply the label. The MCP tool failure is still reproducible against the same issue number.Release classification: not release-blocking; agent-ops hardening. The REST fallback exists, but the MCP surface should not fail its primary issue-label use case.
Live latest-open sweep: checked the latest 20 open issues on 2026-06-14T20:21Z; no equivalent open issue found. A2A in-flight sweep: checked latest 30 messages; no active claim/intent for this scope found.
The Problem
manage_issue_labelsis documented as a unified issue-or-pull-request label tool. The merged support from#10077/ PR#11696covered the old PR-label failure class, but the current live failure is the mirror image: an issue-only number fails because GitHub GraphQL rejects thepullRequest(number:)field when no PR with that repository number exists.The current unit tests mock the dual lookup as if GitHub returns
{ issue, pullRequest: null }or{ issue: null, pullRequest }. Live GraphQL can instead fail the whole operation when one branch of the combined query cannot resolve, so the fallback expression never executes.The Architectural Reality
Current
origin/devshape:ai/mcp/server/github-workflow/toolService.mjsmapsmanage_issue_labelstoIssueService.manageIssueLabels().ai/services/github-workflow/IssueService.mjs#manageIssueLabels()delegates add/remove toaddLabels()/removeLabels().IssueService.#getIds()calls one combinedGET_ISSUE_AND_LABEL_IDSquery, then choosesdata.repository.issue?.id || data.repository.pullRequest?.id.ai/services/github-workflow/queries/issueQueries.mjsdefinesGET_ISSUE_AND_LABEL_IDSwith bothissue(number: $issueNumber)andpullRequest(number: $issueNumber)in the same query.test/playwright/unit/ai/services/github-workflow/IssueService.spec.mjshas contract coverage for the intended dual behavior, but not for the live GraphQL partial-field rejection path.The Fix
Make
manage_issue_labelsresolve the labelable target without relying on a single combined issue+PR query that can fail on the missing side.Likely shape:
Contract Ledger Matrix
manage_issue_labelsMCP toolIssueService.#getIds()labelable resolutionIssueService.manageIssueLabels()implementationLabelableID without a combined issue+PR query that can be rejected by GitHub for one missing side.IssueService.spec.mjstests with mockedGraphqlService.query()call order and error shapes.Decision Record impact
None. This is a bug fix in an existing MCP service surface and does not amend an ADR.
Acceptance Criteria
manage_issue_labelssucceeds for a real issue number even when no PR with the same number exists.manage_issue_labelsstill succeeds for a PR number.Could not resolve to a PullRequest with the number of 13260.Out of Scope
maxRepoLabelscontract.Related
Related: #10077, #11696
Retrieval Hint:
manage_issue_labels issue-only number PullRequest resolution GraphQL partial field rejection