Phase 1: Implement MCP Tool for Conflict Data
Overview
Create the get_merge_request_conflicts MCP tool (ID 88) as a CustomService that reuses existing infrastructure - no logic duplication.
Architecture Decision: CustomService (Not REST API)
We're implementing this as an MCP Tool (CustomService) because:
MergeRequests::Conflicts::ListService (same as web UI)
McpTool class handles it automatically
Shared Service Architecture
┌─────────────────────────────────────────┐
│ MergeRequests::Conflicts::ListService │ ← Existing logic
│ (battle-tested, powers web UI) │
└─────────────────────────────────────────┘
↑ ↑
│ │
┌──────┴─────┐ ┌─────┴──────────┐
│ Controller │ │ CustomService │ ← What we're adding
│ (Web UI) │ │ (MCP Tool) │
│ EXISTING │ │ NEW (~50 lines) │
└────────────┘ └─────────────────┘
Both call the exact same service - no duplication!
Tasks
Optional: Model Enhancement (Makes code cleaner)
-
Add
conflict_list_servicedelegation method to MergeRequest model -
Add
structured_conflictsconvenience method to MergeRequest model - Write specs for new MergeRequest methods
Note: This is optional but makes the CustomService code cleaner. Without it, we just call ListService.new(merge_request) directly.
CustomService Implementation (Core Work)
-
Create
app/services/mcp/tools/get_merge_request_conflicts_service.rb -
Extend
CustomServicebase class -
Implement
auth_abilitymethod (return:read_merge_request) -
Implement
auth_targetmethod (return the merge request) -
Implement
perform_0_1_0method that:- Finds the merge request
- Creates
ListService.new(merge_request)← Same as controller! - Checks
can_be_resolved_in_ui?← Same as controller! - Returns
to_json← Same as controller!
- Handle error cases (no conflicts, binary conflicts, not found)
- Write comprehensive specs
Tool Registration
-
Register tool in
CUSTOM_TOOLShash inapp/services/mcp/tools/manager.rb -
Add tool definition (ID 88) to
ee/lib/ai/catalog/built_in_tool_definitions.rb
Testing
- Test tool manually in Rails console
- Verify tool returns same structure as controller (line 24)
- Test with various conflict scenarios (simple, multi-file, binary)
- Verify authorization checks work
- Test error handling
Code Reference: Reusing Controller Logic
ConflictsController (Existing - line 22-37)
format.json do
if @conflicts_list.can_be_resolved_in_ui?
render json: @conflicts_list # ← Line 24: This is what we reuse!
else
render json: { message: 'Cannot resolve...', type: 'error' }
end
end
CustomService (What We're Building)
class GetMergeRequestConflictsService < CustomService
def perform_0_1_0(arguments)
merge_request = find_merge_request(arguments)
# Same service as controller line 81!
conflicts_list = ::MergeRequests::Conflicts::ListService.new(merge_request)
# Same check as controller lines 23-24!
unless conflicts_list.can_be_resolved_in_ui?
return error_response('Cannot resolve in UI')
end
# Same output as controller line 24!
success_response(conflicts_list.to_json)
end
end
Exact same logic - just different entry point!
Acceptance Criteria
- CustomService tool exists and is registered
-
Tool reuses
MergeRequests::Conflicts::ListService(no duplication) - Tool returns same JSON structure as controller (line 24)
- Tool is discoverable via MCP
- Tool returns structured conflict data with markers for resolvable conflicts
- Tool returns appropriate errors for non-resolvable or non-existent conflicts
- All specs pass with >90% coverage
- Tool works in Rails console testing
-
Python automatically wraps it via
McpTool(no Python code needed)
Implementation Notes
Why This Works
-
Controller uses:
ListService.new(merge_request).to_json -
CustomService uses:
ListService.new(merge_request).to_json - Result: Same data, same format, zero duplication!
What Gets Reused
-
✅ MergeRequests::Conflicts::ListService- Main service -
✅ Gitlab::Conflict::FileCollection- Conflict data structure -
✅ Gitlab::Conflict::File- Individual file conflicts -
✅ Authorization logic pattern -
✅ Error handling patterns
What's New (Just Wrapper Code)
- MCP tool input validation (~10 lines)
- MCP tool response formatting (~10 lines)
- Tool registration (~5 lines)
- Tool definition in catalog (~10 lines)
- Total new code: ~50-60 lines
Files Changed
New Files:
-
app/services/mcp/tools/get_merge_request_conflicts_service.rb(~50 lines) -
spec/services/mcp/tools/get_merge_request_conflicts_service_spec.rb(~100 lines)
Modified Files:
-
app/models/merge_request.rb(optional: +10 lines) -
spec/models/merge_request_spec.rb(optional: +30 lines) -
app/services/mcp/tools/manager.rb(+1 line) -
ee/lib/ai/catalog/built_in_tool_definitions.rb(+15 lines)
Reused (No Changes Needed):
-
app/services/merge_requests/conflicts/list_service.rb← Battle-tested! -
lib/gitlab/conflict/file_collection.rb← Battle-tested! -
lib/gitlab/conflict/file.rb← Battle-tested!
Testing Checklist
-
Tool returns same data as
ConflictsController#show(format.json) - Tool works with project ID (numeric)
- Tool works with project path (namespace/project)
- Tool handles already-resolved conflicts gracefully
- Tool handles binary conflicts gracefully
- Tool handles missing merge requests gracefully
- Tool respects authorization (fails for unauthorized users)
-
Tool registered in
CUSTOM_TOOLS -
Tool defined in
built_in_tool_definitions.rb -
Python
McpToolwrapper can call it (verify in logs)
Timeline
2 days
Day 1:
- Create CustomService (~2 hours)
- Register tool (~30 min)
- Write tests (~3 hours)
- Manual testing (~1 hour)
Day 2:
- Fix any issues (~2 hours)
- Code review feedback (~2 hours)
- Documentation (~1 hour)
Related to epic &20688