Skip to main content
Glama

GitLab Code Review MCP

by lininn
server.py12.7 kB
import os import json import logging from typing import Optional, Dict, Any, Union, List from dataclasses import dataclass from contextlib import asynccontextmanager from collections.abc import AsyncIterator from urllib.parse import quote import requests from dotenv import load_dotenv from mcp.server.fastmcp import FastMCP, Context # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Load environment variables load_dotenv() @dataclass class GitLabContext: host: str token: str api_version: str = "v4" def make_gitlab_api_request(ctx: Context, endpoint: str, method: str = "GET", data: Optional[Dict[str, Any]] = None) -> Any: """Make a REST API request to GitLab and handle the response""" gitlab_ctx = ctx.request_context.lifespan_context if not gitlab_ctx.token: logger.error("GitLab token not set in context") raise ValueError("GitLab token not set. Please set GITLAB_TOKEN in your environment.") url = f"https://{gitlab_ctx.host}/api/{gitlab_ctx.api_version}/{endpoint}" headers = { 'Accept': 'application/json', 'User-Agent': 'GitLabMCPCodeReview/1.0', 'Private-Token': gitlab_ctx.token } try: logger.info(f"Making {method} request to {url}") logger.debug(f"Headers: {headers}") response = None if method.upper() == "GET": response = requests.get(url, headers=headers, verify=True) elif method.upper() == "POST": logger.debug(f"Request data: {data}") response = requests.post(url, headers=headers, json=data, verify=True) else: raise ValueError(f"Unsupported HTTP method: {method}") if response is None: logger.error("Request did not return a response.") raise Exception("Request did not return a response.") if response.status_code == 401: logger.error("Authentication failed. Check your GitLab token.") raise Exception("Authentication failed. Please check your GitLab token.") response.raise_for_status() if not response.content: return {} try: return response.json() except json.JSONDecodeError as e: logger.error(f"Failed to parse JSON response: {str(e)}") raise Exception(f"Failed to parse GitLab response as JSON: {str(e)}") except requests.exceptions.RequestException as e: logger.error(f"REST request failed: {str(e)}") if hasattr(e, 'response'): logger.error(f"Response status: {e.response.status_code}") raise Exception(f"Failed to make GitLab API request: {str(e)}") @asynccontextmanager async def gitlab_lifespan(server: FastMCP) -> AsyncIterator[GitLabContext]: """Manage GitLab connection details""" host = os.getenv("GITLAB_HOST", "gitlab.com") token = os.getenv("GITLAB_TOKEN", "") if not token: logger.error("Missing required environment variable: GITLAB_TOKEN") raise ValueError( "Missing required environment variable: GITLAB_TOKEN. " "Please set this in your environment or .env file." ) ctx = GitLabContext(host=host, token=token) try: yield ctx finally: pass # Create MCP server mcp = FastMCP( name="GitLab MCP for Code Review", instructions="MCP server for reviewing GitLab code changes", lifespan=gitlab_lifespan, dependencies=["python-dotenv", "requests"] ) @mcp.tool() def fetch_merge_request(ctx: Context, project_id: str, merge_request_iid: str) -> Dict[str, Any]: """ Fetch a GitLab merge request and its contents. Args: project_id: The GitLab project ID or URL-encoded path merge_request_iid: The merge request IID (project-specific ID) Returns: Dict containing the merge request information """ # Get merge request details mr_endpoint = f"projects/{quote(project_id, safe='')}/merge_requests/{merge_request_iid}" mr_info = make_gitlab_api_request(ctx, mr_endpoint) if not mr_info: raise ValueError(f"Merge request {merge_request_iid} not found in project {project_id}") # Get the changes (diffs) for this merge request changes_endpoint = f"{mr_endpoint}/changes" changes_info = make_gitlab_api_request(ctx, changes_endpoint) # Get the commit information commits_endpoint = f"{mr_endpoint}/commits" commits_info = make_gitlab_api_request(ctx, commits_endpoint) # Get the notes (comments) for this merge request notes_endpoint = f"{mr_endpoint}/notes" notes_info = make_gitlab_api_request(ctx, notes_endpoint) return { "merge_request": mr_info, "changes": changes_info, "commits": commits_info, "notes": notes_info } @mcp.tool() def fetch_merge_request_diff(ctx: Context, project_id: str, merge_request_iid: str, file_path: Optional[str] = None) -> Dict[str, Any]: """ Fetch the diff for a specific file in a merge request, or all files if none specified. Args: project_id: The GitLab project ID or URL-encoded path merge_request_iid: The merge request IID (project-specific ID) file_path: Optional specific file path to get diff for Returns: Dict containing the diff information """ # Get the changes for this merge request changes_endpoint = f"projects/{quote(project_id, safe='')}/merge_requests/{merge_request_iid}/changes" changes_info = make_gitlab_api_request(ctx, changes_endpoint) if not changes_info: raise ValueError(f"Changes not found for merge request {merge_request_iid}") # Extract all changes files = changes_info.get("changes", []) # Filter by file path if specified if file_path: files = [f for f in files if f.get("new_path") == file_path or f.get("old_path") == file_path] if not files: raise ValueError(f"File '{file_path}' not found in the merge request changes") return { "merge_request_iid": merge_request_iid, "files": files } @mcp.tool() def fetch_commit_diff(ctx: Context, project_id: str, commit_sha: str, file_path: Optional[str] = None) -> Dict[str, Any]: """ Fetch the diff for a specific commit, or for a specific file in that commit. Args: project_id: The GitLab project ID or URL-encoded path commit_sha: The commit SHA file_path: Optional specific file path to get diff for Returns: Dict containing the diff information """ # Get the diff for this commit diff_endpoint = f"projects/{quote(project_id, safe='')}/repository/commits/{commit_sha}/diff" diff_info = make_gitlab_api_request(ctx, diff_endpoint) if not diff_info: raise ValueError(f"Diff not found for commit {commit_sha}") # Filter by file path if specified if file_path: diff_info = [d for d in diff_info if d.get("new_path") == file_path or d.get("old_path") == file_path] if not diff_info: raise ValueError(f"File '{file_path}' not found in the commit diff") # Get the commit details commit_endpoint = f"projects/{quote(project_id, safe='')}/repository/commits/{commit_sha}" commit_info = make_gitlab_api_request(ctx, commit_endpoint) return { "commit": commit_info, "diffs": diff_info } @mcp.tool() def compare_versions(ctx: Context, project_id: str, from_sha: str, to_sha: str) -> Dict[str, Any]: """ Compare two commits/branches/tags to see the differences between them. Args: project_id: The GitLab project ID or URL-encoded path from_sha: The source commit/branch/tag to_sha: The target commit/branch/tag Returns: Dict containing the comparison information """ # Compare the versions compare_endpoint = f"projects/{quote(project_id, safe='')}/repository/compare?from={quote(from_sha, safe='')}&to={quote(to_sha, safe='')}" compare_info = make_gitlab_api_request(ctx, compare_endpoint) if not compare_info: raise ValueError(f"Comparison failed between {from_sha} and {to_sha}") return compare_info @mcp.tool() def add_merge_request_comment(ctx: Context, project_id: str, merge_request_iid: str, body: str, position: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """ Add a comment to a merge request, optionally at a specific position in a file. Args: project_id: The GitLab project ID or URL-encoded path merge_request_iid: The merge request IID (project-specific ID) body: The comment text position: Optional position data for line comments Returns: Dict containing the created comment information """ # Create the comment data data = { "body": body } # Add position data if provided if position: data["position"] = position # Add the comment comment_endpoint = f"projects/{quote(project_id, safe='')}/merge_requests/{merge_request_iid}/notes" comment_info = make_gitlab_api_request(ctx, comment_endpoint, method="POST", data=data) if not comment_info: raise ValueError("Failed to add comment to merge request") return comment_info @mcp.tool() def approve_merge_request(ctx: Context, project_id: str, merge_request_iid: str, approvals_required: Optional[int] = None) -> Dict[str, Any]: """ Approve a merge request. Args: project_id: The GitLab project ID or URL-encoded path merge_request_iid: The merge request IID (project-specific ID) approvals_required: Optional number of required approvals to set Returns: Dict containing the approval information """ # Approve the merge request approve_endpoint = f"projects/{quote(project_id, safe='')}/merge_requests/{merge_request_iid}/approve" approve_info = make_gitlab_api_request(ctx, approve_endpoint, method="POST") # Set required approvals if specified if approvals_required is not None: approvals_endpoint = f"projects/{quote(project_id, safe='')}/merge_requests/{merge_request_iid}/approvals" data = { "approvals_required": approvals_required } make_gitlab_api_request(ctx, approvals_endpoint, method="POST", data=data) return approve_info @mcp.tool() def unapprove_merge_request(ctx: Context, project_id: str, merge_request_iid: str) -> Dict[str, Any]: """ Unapprove a merge request. Args: project_id: The GitLab project ID or URL-encoded path merge_request_iid: The merge request IID (project-specific ID) Returns: Dict containing the unapproval information """ # Unapprove the merge request unapprove_endpoint = f"projects/{quote(project_id, safe='')}/merge_requests/{merge_request_iid}/unapprove" unapprove_info = make_gitlab_api_request(ctx, unapprove_endpoint, method="POST") return unapprove_info @mcp.tool() def get_project_merge_requests(ctx: Context, project_id: str, state: str = "all", limit: int = 20) -> List[Dict[str, Any]]: """ Get all merge requests for a project. Args: project_id: The GitLab project ID or URL-encoded path state: Filter merge requests by state (all, opened, closed, merged, or locked) limit: Maximum number of merge requests to return Returns: List of merge request objects """ # Get the merge requests mrs_endpoint = f"projects/{quote(project_id, safe='')}/merge_requests?state={state}&per_page={limit}" mrs_info = make_gitlab_api_request(ctx, mrs_endpoint) return mrs_info @mcp.tool() def get_review_guidelines(ctx: Context) -> str: """ Get the code review guidelines. Returns: The content of the code review guidelines file. """ try: with open("CODE_REVIEW_GUIDELINES.md", "r", encoding="utf-8") as f: return f.read() except FileNotFoundError: logger.error("CODE_REVIEW_GUIDELINES.md not found.") raise FileNotFoundError("CODE_REVIEW_GUIDELINES.md not found.") except Exception as e: logger.error(f"Failed to read CODE_REVIEW_GUIDELINES.md: {str(e)}") raise if __name__ == "__main__": try: logger.info("Starting GitLab Review MCP server") # Initialize and run the server mcp.run(transport='stdio') except Exception as e: logger.error(f"Failed to start MCP server: {str(e)}") raise

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/lininn/gitlab-code-review-mcp'

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