Add full_patch diff detail to get_merge_request facet
What does this MR do and why?
Adds a detail: full_patch option to the get_merge_request MCP tool's diffs facet.
The facet previously returned change stats only (summary totals and per-file additions and
deletions); raw per-file patch text was reachable only through the standalone
get_merge_request_diffs tool. full_patch consumes the new MergeRequest.diffs GraphQL
field (added in the stacked MR below) and returns per-file patch text alongside the
per-file stats, so an agent can read an MR's diffs inline without a second tool call.
Patch text is paginated: diffs_first sets the page size (default 20, max 100) and
diffs_after takes the cursor from pageInfo.endCursor.
This is additive: the standalone get_merge_request_diffs tool is left fully in place.
The facet and the standalone tool coexist.
An EE override of the tool reuses Ai::FileExclusionService so files a project excludes
from Duo context are stripped from the patch text, matching what the REST endpoint's
filter_diffs_for_mcp does.
Part B of the diffs work tracked by #605878 (closed).
⚠️ Stacked on the MR that addsMergeRequest.diffs(branch605878-diffs-full-patch-field, !248993 (merged)). Review/merge that first — this MR's.graphqlquery does not compile without it.
What changed
- New
detailparameter (get_merge_request_service.rb), enum%w[none stats full_patch], applies only whenincludeis["diffs"]:none— summary counts only.stats— summary plus per-file additions and deletions. This is the default, so the facet's existing behavior is unchanged whendetailis omitted.full_patch— adds raw per-file patch text.
- GraphQL variables (
get_merge_request_tool.rb):detailmaps toincludeDiffFiles(true unlessdetail: none) andincludeDiffPatches(true only forfull_patch), which gate thediffStatsand the newdiffs { nodes { … diff } pageInfo { … } }selections.includekeeps the base's capped-array shape (Array(params[:include])). - Exclusion parity lives in
ee/app/services/ee/mcp/tools/merge_requests/get_merge_request_tool.rb, which post-filters the returneddiffsthroughAi::FileExclusionService— the same service the REST diffs endpoint uses viafilter_diffs_for_mcp. Files excluded from a project's Duo context are omitted fromfull_patch. - Pagination.
diffs(first:, after:)withnodesandpageInfo, wired to the newdiffs_first/diffs_afterparameters.pageInfo.hasNextPagemeans a large merge request reports that more files exist instead of silently returning a partial diff. This is the same class of defect as #599320, where an agent flow silently missed everything past the first 20 files; that issue is about the Pythonlist_merge_request_diffstool inai-assist, so this does not close it, but the facet does not repeat the mistake. - No tool removed.
get_merge_request_diffskeeps its registration,route_setting :mcp,GITLAB_PREAPPROVED_TOOLSentry,list_tools/call_toolcoverage, and doc section. - Complexity override raised 260 → 400 in
ee/spec/graphql/all_queries_spec.rb. The static analyzer sums every@includefacet even though only one is ever active per call, and a connection additionally multiplies its subtree by the page size, taking the computed score to 384. The override and its "false positive" rationale were introduced by the base MR; this widens it. - Multiversion compatibility warning is a false positive here. Danger flags MRs touching both
a
.graphqldocument and backend schema code, because a frontend can deploy ahead of the backend.app/graphql/queries/mcp/is not frontend code — it is a server-side query document executed by the MCP tool inside the same Rails process, so the query and the schema always deploy together.
MCP tool proposal: this extends an existing tool's input schema additively (a new
optional detail parameter and a new enum value) rather than adding a net-new tool, so no
separate MCP Tool Proposal issue is required.
References
- MCP tool issue: Add `diffs` include facet to get_merge_request ... (#611499 - closed)
- Related: #599320 (same silent-truncation class, different tool/codebase)
- Parent issue: #605878 (closed)
- Parent MR (base): !248373 (merged)
- Depends on the stacked GraphQL field MR: !248993 (merged).
Screenshots or screen recordings
No UI changes.
| Before | After |
|---|---|
How to set up and validate locally
Agentic validation
Using a script to test this out: $6036632
Testing using sonnet
>> Minting a local-GDK PAT (api+mcp) — boots Rails, ~30-60s...
>> Preflight OK. Project: top-level-public/top-project-public target branch: main
>> Seeding branch mcp-getmr-1787169146 with 4 files...
>> Seeded MR !10 (diff populated)
==================================================================
A. include=[diffs], detail omitted -> default is stats (per-file add/del, NO patch text)
PASS A returns success
PASS A diffStatsSummary reports changed files
PASS A has per-file diffStats
PASS A has NO diffs patch block (correct for stats)
B. detail=none -> summary counts only (no per-file stats, no patches)
PASS B returns success
PASS B has diffStatsSummary
PASS B has NO per-file diffStats
PASS B has NO diffs patch block
C. detail=full_patch -> raw per-file patch text + stats + pageInfo
PASS C returns success
PASS C still includes per-file diffStats
PASS C diffs.nodes present
PASS C at least one node has non-empty patch text (4)
PASS C has pageInfo.endCursor
D. pagination: full_patch + diffs_first=2 -> capped page, hasNextPage, working cursor
PASS D page 1 returns success
PASS D page 1 capped to 2 (<=2)
PASS D page 1 hasNextPage=true
PASS D page 1 endCursor present
PASS D page 2 returns success
PASS D page 2 returns different files (no overlap)
E. detail is a no-op without diffs in include (include=[commits], detail=full_patch)
PASS E returns success
PASS E no diffs block
PASS E no diff summary (diffs not requested)
F. url form resolves the same MR with full_patch
PASS F url-form returns success
PASS F resolved MR !10 via url
==================================================================
G. agentic smoke (needs ANTHROPIC_API_KEY)
------------------------------------------------------------------
AGENTIC: read patches inline [model=sonnet, 3 runs]
PROMPT: Show me the actual code changes (the raw patch/diff text) in merge request !10 of project top-level-public/top-project-public. I want to read the diff hunks inline, not just a summary.
run 1: mcp__gdk__get_merge_request_diffs {"id":"top-level-public/top-project-public","merge_request_iid":10}
run 2: mcp__gdk__get_merge_request_diffs {"id":"top-level-public/top-project-public","merge_request_iid":10}
run 3: mcp__gdk__get_merge_request {"project_id":"top-level-public/top-project-public","merge_request_iid":10,"include":["diffs"],"detail":"full_patch"}
TALLY get_merge_request=1 get_merge_request_diffs=2 no-tool=0 | runs-that-got-patch-text=3/3
------------------------------------------------------------------
AGENTIC: review the diff [model=sonnet, 3 runs]
PROMPT: I'm reviewing merge request !10 in top-level-public/top-project-public. Pull its per-file changes so you can quote the changed lines back to me.
run 1: mcp__gdk__get_merge_request {"project_id":"top-level-public/top-project-public","merge_request_iid":10,"include":["diffs"],"detail":"full_patch"}
run 2: mcp__gdk__get_merge_request {"project_id":"top-level-public/top-project-public","merge_request_iid":10,"include":["diffs"],"detail":"full_patch"}
run 3: mcp__gdk__get_merge_request_diffs {"id":"top-level-public/top-project-public","merge_request_iid":10}
TALLY get_merge_request=2 get_merge_request_diffs=1 no-tool=0 | runs-that-got-patch-text=3/3
==================================================================
H. EE file exclusion: an excluded file drops out of full_patch (opt-in: TEST_EXCLUSION=1)
PASS H returns success
PASS H excluded file mcp_diff_1.txt dropped from full_patch
PASS H non-excluded file mcp_diff_2.txt still present
restored prior exclusion setting
==================================================================
>> Cleanup...
closed MR !10
deleted branch mcp-getmr-1787169146
revoked test PAT
==================================================================
RESULT: 27 passed, 0 failedTesting using opus
>> Minting a local-GDK PAT (api+mcp) — boots Rails, ~30-60s...
>> Preflight OK. Project: top-level-public/top-project-public target branch: main
>> Seeding branch mcp-getmr-1787169319 with 4 files...
>> Seeded MR !11 (diff populated)
==================================================================
A. include=[diffs], detail omitted -> default is stats (per-file add/del, NO patch text)
PASS A returns success
PASS A diffStatsSummary reports changed files
PASS A has per-file diffStats
PASS A has NO diffs patch block (correct for stats)
B. detail=none -> summary counts only (no per-file stats, no patches)
PASS B returns success
PASS B has diffStatsSummary
PASS B has NO per-file diffStats
PASS B has NO diffs patch block
C. detail=full_patch -> raw per-file patch text + stats + pageInfo
PASS C returns success
PASS C still includes per-file diffStats
PASS C diffs.nodes present
PASS C at least one node has non-empty patch text (4)
PASS C has pageInfo.endCursor
D. pagination: full_patch + diffs_first=2 -> capped page, hasNextPage, working cursor
PASS D page 1 returns success
PASS D page 1 capped to 2 (<=2)
PASS D page 1 hasNextPage=true
PASS D page 1 endCursor present
PASS D page 2 returns success
PASS D page 2 returns different files (no overlap)
E. detail is a no-op without diffs in include (include=[commits], detail=full_patch)
PASS E returns success
PASS E no diffs block
PASS E no diff summary (diffs not requested)
F. url form resolves the same MR with full_patch
PASS F url-form returns success
PASS F resolved MR !11 via url
==================================================================
G. agentic smoke (needs ANTHROPIC_API_KEY)
------------------------------------------------------------------
AGENTIC: read patches inline [model=opus, 3 runs]
PROMPT: Show me the actual code changes (the raw patch/diff text) in merge request !11 of project top-level-public/top-project-public. I want to read the diff hunks inline, not just a summary.
run 1: mcp__gdk__get_merge_request {"project_id":"top-level-public/top-project-public","merge_request_iid":11,"include":["diffs"],"detail":"full_patch"}
run 2: mcp__gdk__get_merge_request {"project_id":"top-level-public/top-project-public","merge_request_iid":11,"include":["diffs"],"detail":"full_patch"}
run 3: mcp__gdk__get_merge_request {"project_id":"top-level-public/top-project-public","merge_request_iid":11,"include":["diffs"],"detail":"full_patch"}
TALLY get_merge_request=3 get_merge_request_diffs=0 no-tool=0 | runs-that-got-patch-text=3/3
------------------------------------------------------------------
AGENTIC: review the diff [model=opus, 3 runs]
PROMPT: I'm reviewing merge request !11 in top-level-public/top-project-public. Pull its per-file changes so you can quote the changed lines back to me.
run 1: mcp__gdk__get_merge_request {"project_id":"top-level-public/top-project-public","merge_request_iid":11,"include":["diffs"],"detail":"full_patch"}
run 2: mcp__gdk__get_merge_request {"project_id":"top-level-public/top-project-public","merge_request_iid":11,"include":["diffs"],"detail":"full_patch"}
run 3: mcp__gdk__get_merge_request {"project_id":"top-level-public/top-project-public","merge_request_iid":11,"include":["diffs"],"detail":"full_patch"}
TALLY get_merge_request=3 get_merge_request_diffs=0 no-tool=0 | runs-that-got-patch-text=3/3
==================================================================
H. EE file exclusion: an excluded file drops out of full_patch (opt-in: TEST_EXCLUSION=1)
PASS H returns success
PASS H excluded file mcp_diff_1.txt dropped from full_patch
PASS H non-excluded file mcp_diff_2.txt still present
restored prior exclusion setting
==================================================================
>> Cleanup...
closed MR !11
deleted branch mcp-getmr-1787169319
revoked test PAT
==================================================================
RESULT: 27 passed, 0 failedManual testing
- Restart Rails so the tool registry re-memoizes (
gdk restart rails-web). - List tools and confirm
get_merge_request_diffsis still present andget_merge_requestadvertises thedetailparameter withfull_patch:curl -s -X POST "$GDK_URL/api/v4/mcp" -H "Authorization: Bearer $PAT" \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' - Call the facet and confirm per-file patch text comes back (
includeis an array; integer params needas: :json):curl -s -X POST "$GDK_URL/api/v4/mcp" -H "Authorization: Bearer $PAT" \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_merge_request","arguments":{"project_id":"gitlab-org/gitlab","merge_request_iid":1,"include":["diffs"],"detail":"full_patch"}}}' - Page a large merge request: pass
diffs_first: 5, then send the returnedpageInfo.endCursorback asdiffs_afterand confirm you get different files and thatpageInfo.hasNextPageis accurate. - Set a project Duo context exclusion rule and confirm the excluded file's patch is absent.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.