Skip to main content
Glama
ffpy

GitLab MCP Code Review

by ffpy

fetch_merge_request

Retrieve a GitLab merge request and its contents to analyze code changes for review. Use after checking team standards to ensure compliance with guidelines.

Instructions

Fetch a GitLab merge request and its contents.

IMPORTANT: You MUST call fetch_code_review_rules BEFORE using this tool to understand 
the team's code review standards and guidelines.

Args:
    project_id: The GitLab project ID or URL-encoded path
    merge_request_iid: The merge request IID (project-specific ID)
Returns:
    XML string containing the merge request information

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
project_idYes
merge_request_iidYes

Implementation Reference

  • The handler function decorated with @mcp.tool() that implements the core logic for fetching and processing GitLab merge request data, filtering changes, collecting commits and discussions, and formatting the output as XML.
    @mcp.tool()
    def fetch_merge_request(ctx: Context, project_id: str, merge_request_iid: str):
        """
        Fetch a GitLab merge request and its contents.
        
        IMPORTANT: You MUST call fetch_code_review_rules BEFORE using this tool to understand 
        the team's code review standards and guidelines.
        
        Args:
            project_id: The GitLab project ID or URL-encoded path
            merge_request_iid: The merge request IID (project-specific ID)
        Returns:
            XML string containing the merge request information
        """
        gl = ctx.request_context.lifespan_context
        project = gl.projects.get(project_id)
        mr = project.mergerequests.get(merge_request_iid)
    
        # 精简 merge_request 信息
        mr_data = mr.asdict()
        slim_mr = {
            "id": mr_data.get("id"),
            "iid": mr_data.get("iid"),
            "project_id": mr_data.get("project_id"),
            "title": mr_data.get("title"),
            "description": mr_data.get("description"),
            "state": mr_data.get("state"),
            "author": mr_data.get("author", {}).get("name"),
            "source_branch": mr_data.get("source_branch"),
            "target_branch": mr_data.get("target_branch"),
        }
    
        # 获取并过滤 changes
        original_changes_data = mr.changes()
        all_changes = original_changes_data.get("changes", [])
    
        exclude_patterns = config.get("exclude_patterns", [])
    
        filtered_changes_list = []
        for change in all_changes:
            file_path = change.get("new_path")
            if not is_path_excluded(file_path, exclude_patterns):
                slim_change = {
                    "new_path": change.get("new_path"),
                    "old_path": change.get("old_path"),
                    "new_file": change.get("new_file"),
                    "renamed_file": change.get("renamed_file"),
                    "deleted_file": change.get("deleted_file"),
                    "diff": change.get("diff")
                }
                filtered_changes_list.append(slim_change)
        
        # 创建一个只包含必要字段的精简版 changes 对象
        slim_changes_obj = {
            "diff_refs": original_changes_data.get("diff_refs"),
            "changes": filtered_changes_list
        }
    
        # 精简 commits
        commits = [
            {
                "id": c.id,
                "short_id": c.short_id,
                "title": c.title,
                "author_name": c.author_name,
            }
            for c in mr.commits(all=True)
        ]
    
        def slim_note(note):
            if not isinstance(note, dict):
                note = note.asdict()
            author = note.get("author", {})
            return {
                "id": note.get("id"),
                "type": note.get("type"),
                "body": note.get("body"),
                "system": note.get("system"),
                "author": author.get("name"),
                "position": note.get("position", {}),
            }
    
        # 精简 discussions 和其下的 notes
        all_discussions = mr.discussions.list(all=True)
    
        discussions = []
        for d in all_discussions:
            # d.attributes['notes'] 包含了该 discussion 下的所有 note 信息
            slim_notes_list = [slim_note(n) for n in d.attributes.get('notes', [])]
            discussions.append({
                "id": d.id,
                "individual_note": d.individual_note,
                "notes": slim_notes_list
            })
    
        # 构建最终的数据结构
        result_data = {
            "merge_request": slim_mr,
            "changes": slim_changes_obj,
            "commits": commits,
            "discussions": discussions,
        }
        
        # 转换为XML并返回
        xml_parts = ['<?xml version="1.0" encoding="utf-8"?>\n<merge_request_data>\n']
        for key, value in result_data.items():
            xml_parts.append(dict_to_xml_string(value, key, 1))
        xml_parts.append('</merge_request_data>\n')
        
        return "".join(xml_parts)
  • Helper function used by fetch_merge_request to recursively convert the merge request data dictionary into an XML-formatted string.
    def dict_to_xml_string(data: Any, tag: str = "item", indent: int = 0) -> str:
        """
        Convert a dictionary or list to XML string format without escaping any characters.
        This output is intended for AI consumption, not for XML parser.
        
        Args:
            data: The data to convert (dict, list, or primitive type)
            tag: The tag name for the current element
            indent: Current indentation level
        Returns:
            The XML string
        """
        indent_str = "  " * indent
        result = []
        
        if isinstance(data, dict):
            result.append(f"{indent_str}<{tag}>\n")
            for key, value in data.items():
                if value is not None:
                    result.append(dict_to_xml_string(value, str(key), indent + 1))
            result.append(f"{indent_str}</{tag}>\n")
        elif isinstance(data, list):
            result.append(f"{indent_str}<{tag}>\n")
            for item in data:
                result.append(dict_to_xml_string(item, "item", indent + 1))
            result.append(f"{indent_str}</{tag}>\n")
        else:
            # Leaf node with text content - no escaping
            if data is None:
                text = ""
            elif isinstance(data, bool):
                text = "true" if data else "false"
            else:
                text = str(data)
            result.append(f"{indent_str}<{tag}>{text}</{tag}>\n")
        
        return "".join(result)
  • Helper function used in fetch_merge_request to filter out excluded file paths from the merge request changes based on config patterns.
    def is_path_excluded(file_path: str, patterns: List[str]) -> bool:
        """Check if a file path matches any of the exclusion patterns."""
        for pattern in patterns:
            if pattern.endswith('/'):
                if file_path.startswith(pattern) or f"/{pattern}" in file_path:
                    return True
            elif fnmatch.fnmatch(file_path, pattern):
                return True
        return False

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ffpy/gitlab-mcp-code-review'

If you have feedback or need assistance with the MCP directory API, please join our Discord server