Context
#12624 shipped the GitLabClient (REST + GraphQL + PAT auth + pagination) and the real IssueService issue operations (PR #12625, APPROVED, in the merge queue). This ticket is the merge-request twin — the next #11404 sub per @neo-gpt's [epic-resolution] pointer ("#11404 KEEP_OPEN after #12625 — next GitLab sub should be non-micro MR/sync batch"). It brings GitLab's PR-equivalent (Merge Requests) to parity with the issue surface that just landed.
Live latest-open sweep: checked the latest 20 open issues at 2026-06-06; no equivalent MR-ops sub found (#12624 is issues-only; #12456 is the AiConfig-SSOT cleanup; no MergeRequestService / merge_request ticket exists). KB unavailable for the semantic pass — not treated as evidence of absence; the live tracker sweep is the authoritative duplicate gate here.
The Problem
Epic #11404 scopes the GitLab Workflow MCP server to issues + merge requests parity. Issues are done (#12624). The MR surface today is scaffold-only: ai/mcp/server/gitlab-workflow/openapi.yaml declares list_merge_requests / get_merge_request as scaffold entries with no backing service. Without real MR operations, a client-project deployment cannot coordinate the MR lifecycle (review routing, comments, labels, assignees/reviewers) through Neo's Agent-OS substrate — the exact gap the epic exists to close, now half-filled.
The Architectural Reality
Mirror the proven IssueService shape under ai/services/gitlab-workflow/:
MergeRequestService.mjs (new) — extends Neo.core.Base, @singleton; the MR twin of IssueService.mjs. Real ops via the existing GitLabClient (no new client). Private helpers mirror IssueService: #apiErrorResponse / #mutationErrorResponse / #userErrors / GID + label/assignee resolvers.
queries/mrQueries.mjs + queries/mrMutations.mjs (new) — GraphQL for project(fullPath){ mergeRequest(iid){…} mergeRequests{…} }, createNote, mergeRequestSetLabels, mergeRequestSetAssignees, mergeRequestSetReviewers. Sibling to the issue queries shipped in #12624.
ai/services.mjs — add import _GL_MergeRequestService, wrap via makeSafe(_GL_MergeRequestService, gitlabSpec) against the existing gitlabSpec, export. ADR 0019: read resolved AiConfig leaves at the use site; never re-implement / alias / mutate the shared singleton.
openapi.yaml — replace the 2 scaffold MR entries with real op descriptions + response schemas (MergeRequestResponse / MergeRequestCommentResponse / MergeRequestLabelResponse / MergeRequestAssigneeResponse), mirroring the 4 issue response schemas added in #12624.
Reuse, do not rebuild: GitLabClient (ai/services/gitlab-workflow/api/GitLabClient.mjs) is provider-agnostic transport — the MR service consumes it directly (AC6). shared/contentPath.mjs is out of scope here (that's the Syncer sub-task).
The Fix
- New:
ai/services/gitlab-workflow/MergeRequestService.mjs, queries/mrQueries.mjs, queries/mrMutations.mjs
- Edit:
ai/services.mjs (+import / makeSafe / export), ai/mcp/server/gitlab-workflow/openapi.yaml (real MR op descriptions + response schemas)
- Tests:
test/playwright/unit/ai/services/gitlab-workflow/MergeRequestService.spec.mjs (mock-HTTP pattern; auth, GraphQL shape, userErrors, label/assignee resolution) — parity with the IssueService spec
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
Docs |
Evidence |
get_merge_request / list_merge_requests MCP tools |
#11404 epic; IssueService precedent (#12624); GitLab GraphQL mergeRequest/mergeRequests |
Real reads via GitLabClient GraphQL; structured {items,count} for list |
API error → structured #apiErrorResponse (no throw across the MCP boundary) |
openapi.yaml real schemas |
spec covers a mocked read round-trip |
manage_mr_comment (createNote) |
GitLab createNote(noteableId: MR-GID); IssueService manageIssueComment precedent |
Create MR note; resolve MR GID from iid first |
userErrors surfaced via #mutationErrorResponse |
openapi.yaml |
spec asserts userErrors path |
manage_mr_labels / manage_mr_assignees (+ reviewers) |
GitLab mergeRequestSetLabels / …SetAssignees / …SetReviewers; IssueService precedent |
Add/remove via set-mutations; resolve label/user IDs |
partial-resolution → structured error, no silent drop |
openapi.yaml |
spec covers add + remove |
Row-level Surface-Anchor V-B-A: each GraphQL field above is verified against the GitLab GraphQL schema + the IssueService twin already merged-pending in #12625; create_merge_request is deferred (MR creation needs source/target branch + diff semantics beyond this batch — see Out of Scope) unless review says otherwise.
Decision Record impact
aligned-with ADR 0019 (AiConfig reactive Provider SSOT — makeSafe wiring reads resolved leaves; no singleton mutation).
aligned-with ADR 0004 (§1.3 provider-agnostic regeneratable-cache substrate — GitLab-as-source-of-truth generalizes verbatim).
Acceptance Criteria
Out of Scope
MergeRequestSyncer + LocalFileService — epic Sub-task 3, a separate sub (the file-IO + resources/content/gitlab/merge-requests/ substrate). This ticket is the service/tool surface only.
create_merge_request — MR creation (source/target branch + diff semantics) is heavier than the read/comment/label/assignee batch; deferred to a follow-up unless cross-family review pulls it in.
- GitLab Discussions / Pipelines / OIDC — per epic Out of Scope.
Avoided Traps
- Re-implementing
GitLabClient — rejected; the #12624 client is provider-agnostic transport, consumed directly (AC6). A second client is migration debt.
- Claiming live-integration validation — rejected; #12625's reviewer (@neo-gpt) caught exactly this overclaim. JSDoc states best-knowledge-not-yet-integration-validated (AC7).
- Mutating the shared AiConfig singleton — rejected (ADR 0019 / the #12335 live-DB-bleed class); read resolved leaves at use site only.
Related
- Epic parent: #11404 (GitLab Workflow MCP Server parity).
- Issue twin + dependency: #12624 / PR #12625 (the
GitLabClient this reuses; APPROVED, unmerged). Implementation note: branch stacks on the #12624 branch to consume GitLabClient, OR lands after #12625 merges. This is stackable, not a hold — work proceeds on the stacked branch.
- ADRs: 0019 (AiConfig SSOT), 0004 (provider-agnostic substrate).
Authored by Claude Opus 4.8 (Claude Code).
Origin Session ID: a54e89a3-4259-4b41-9e26-561f665de744
Retrieval Hint: "GitLab MergeRequestService merge-request operations GraphQL mergeRequestSetLabels makeSafe gitlabSpec #11404 twin of #12624"
Context
#12624 shipped the
GitLabClient(REST + GraphQL + PAT auth + pagination) and the realIssueServiceissue operations (PR #12625, APPROVED, in the merge queue). This ticket is the merge-request twin — the next #11404 sub per @neo-gpt's[epic-resolution]pointer ("#11404 KEEP_OPEN after #12625 — next GitLab sub should be non-micro MR/sync batch"). It brings GitLab's PR-equivalent (Merge Requests) to parity with the issue surface that just landed.Live latest-open sweep: checked the latest 20 open issues at 2026-06-06; no equivalent MR-ops sub found (#12624 is issues-only; #12456 is the AiConfig-SSOT cleanup; no
MergeRequestService/merge_requestticket exists). KB unavailable for the semantic pass — not treated as evidence of absence; the live tracker sweep is the authoritative duplicate gate here.The Problem
Epic #11404 scopes the GitLab Workflow MCP server to issues + merge requests parity. Issues are done (#12624). The MR surface today is scaffold-only:
ai/mcp/server/gitlab-workflow/openapi.yamldeclareslist_merge_requests/get_merge_requestas scaffold entries with no backing service. Without real MR operations, a client-project deployment cannot coordinate the MR lifecycle (review routing, comments, labels, assignees/reviewers) through Neo's Agent-OS substrate — the exact gap the epic exists to close, now half-filled.The Architectural Reality
Mirror the proven
IssueServiceshape underai/services/gitlab-workflow/:MergeRequestService.mjs(new) —extends Neo.core.Base,@singleton; the MR twin ofIssueService.mjs. Real ops via the existingGitLabClient(no new client). Private helpers mirror IssueService:#apiErrorResponse/#mutationErrorResponse/#userErrors/ GID + label/assignee resolvers.queries/mrQueries.mjs+queries/mrMutations.mjs(new) — GraphQL forproject(fullPath){ mergeRequest(iid){…} mergeRequests{…} },createNote,mergeRequestSetLabels,mergeRequestSetAssignees,mergeRequestSetReviewers. Sibling to the issue queries shipped in #12624.ai/services.mjs— addimport _GL_MergeRequestService, wrap viamakeSafe(_GL_MergeRequestService, gitlabSpec)against the existinggitlabSpec, export. ADR 0019: read resolvedAiConfigleaves at the use site; never re-implement / alias / mutate the shared singleton.openapi.yaml— replace the 2 scaffold MR entries with real op descriptions + response schemas (MergeRequestResponse/MergeRequestCommentResponse/MergeRequestLabelResponse/MergeRequestAssigneeResponse), mirroring the 4 issue response schemas added in #12624.Reuse, do not rebuild:
GitLabClient(ai/services/gitlab-workflow/api/GitLabClient.mjs) is provider-agnostic transport — the MR service consumes it directly (AC6).shared/contentPath.mjsis out of scope here (that's the Syncer sub-task).The Fix
ai/services/gitlab-workflow/MergeRequestService.mjs,queries/mrQueries.mjs,queries/mrMutations.mjsai/services.mjs(+import / makeSafe / export),ai/mcp/server/gitlab-workflow/openapi.yaml(real MR op descriptions + response schemas)test/playwright/unit/ai/services/gitlab-workflow/MergeRequestService.spec.mjs(mock-HTTP pattern; auth, GraphQL shape, userErrors, label/assignee resolution) — parity with the IssueService specContract Ledger Matrix
get_merge_request/list_merge_requestsMCP toolsIssueServiceprecedent (#12624); GitLab GraphQLmergeRequest/mergeRequestsGitLabClientGraphQL; structured{items,count}for list#apiErrorResponse(no throw across the MCP boundary)openapi.yamlreal schemasmanage_mr_comment(createNote)createNote(noteableId: MR-GID); IssueServicemanageIssueCommentprecedent#mutationErrorResponseopenapi.yamlmanage_mr_labels/manage_mr_assignees(+ reviewers)mergeRequestSetLabels/…SetAssignees/…SetReviewers; IssueService precedentopenapi.yamlRow-level Surface-Anchor V-B-A: each GraphQL field above is verified against the GitLab GraphQL schema + the IssueService twin already merged-pending in #12625;
create_merge_requestis deferred (MR creation needs source/target branch + diff semantics beyond this batch — see Out of Scope) unless review says otherwise.Decision Record impact
aligned-withADR 0019 (AiConfig reactive Provider SSOT —makeSafewiring reads resolved leaves; no singleton mutation).aligned-withADR 0004 (§1.3 provider-agnostic regeneratable-cache substrate — GitLab-as-source-of-truth generalizes verbatim).Acceptance Criteria
MergeRequestService.mjsimplements real read + comment + label + assignee/reviewer operations viaGitLabClient(no scaffold responses).queries/mrQueries.mjs+queries/mrMutations.mjscarry the GraphQL with GID resolution mirroring the issue queries.ai/services.mjswraps the service viamakeSafe(..., gitlabSpec)+ exports it; reads resolved AiConfig leaves at use site (ADR 0019), no singleton mutation.openapi.yamlMR ops carry real descriptions + response schemas (no scaffold labels on the implemented ops).IssueService.spec.mjs(auth, GraphQL shape, userErrors, label/assignee resolution) — mock-HTTP, no live instance.GitLabClientdirectly — no duplicate client implementation.Out of Scope
MergeRequestSyncer+LocalFileService— epic Sub-task 3, a separate sub (the file-IO +resources/content/gitlab/merge-requests/substrate). This ticket is the service/tool surface only.create_merge_request— MR creation (source/target branch + diff semantics) is heavier than the read/comment/label/assignee batch; deferred to a follow-up unless cross-family review pulls it in.Avoided Traps
GitLabClient— rejected; the #12624 client is provider-agnostic transport, consumed directly (AC6). A second client is migration debt.Related
GitLabClientthis reuses; APPROVED, unmerged). Implementation note: branch stacks on the #12624 branch to consumeGitLabClient, OR lands after #12625 merges. This is stackable, not a hold — work proceeds on the stacked branch.Authored by Claude Opus 4.8 (Claude Code). Origin Session ID: a54e89a3-4259-4b41-9e26-561f665de744
Retrieval Hint: "GitLab MergeRequestService merge-request operations GraphQL mergeRequestSetLabels makeSafe gitlabSpec #11404 twin of #12624"