Skip to main content
Glama
gitlab_server.py21.2 kB
#!/usr/bin/env python3 """GitLab MCP server example for code review operations.""" import sys import os import json import logging from pathlib import Path from typing import Optional, Dict, Any, List from dataclasses import dataclass from urllib.parse import quote import requests from dotenv import load_dotenv # Add parent directory to path for imports sys.path.insert(0, str(Path(__file__).parent.parent.parent)) import asyncio import mcp.types as types from mcp.server.stdio import stdio_server from mcp_server_hero.core.server import MCPServerHero from mcp_server_hero.middleware.rate_limit import RateLimitMiddleware from mcp_server_hero.auth.base import BaseAuthProvider, AuthContext, Permission # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Load environment variables load_dotenv() @dataclass class GitLabConfig: """GitLab configuration settings.""" host: str token: str api_version: str = "v4" class GitLabAuthProvider(BaseAuthProvider): """Authentication provider for GitLab integration.""" def __init__(self) -> None: super().__init__("gitlab") self.gitlab_token = os.getenv("GITLAB_TOKEN", "") async def authenticate(self, credentials: dict[str, any]) -> AuthContext: """Authenticate using GitLab token.""" token = credentials.get("token", self.gitlab_token) if not token: return AuthContext(authenticated=False) # Verify token by making a test API request try: config = GitLabConfig(host=os.getenv("GITLAB_HOST", "gitlab.com"), token=token) # Test API access url = f"https://{config.host}/api/{config.api_version}/user" headers = {"Accept": "application/json", "Private-Token": token} response = requests.get(url, headers=headers, timeout=10) if response.status_code == 200: user_data = response.json() return AuthContext( user_id=str(user_data.get("id", "unknown")), authenticated=True, permissions={ Permission.ADMIN, Permission.READ_TOOLS, Permission.CALL_TOOLS, Permission.READ_RESOURCES, }, metadata={"username": user_data.get("username", ""), "name": user_data.get("name", "")}, ) else: return AuthContext(authenticated=False) except Exception as e: logger.error(f"GitLab authentication failed: {e}") return AuthContext(authenticated=False) async def authorize(self, context: AuthContext, resource: str, action: str) -> bool: """Authorize access to GitLab resources.""" return context.authenticated def make_gitlab_api_request(endpoint: str, method: str = "GET", data: Optional[Dict[str, Any]] = None) -> Any: """Make a REST API request to GitLab and handle the response.""" config = GitLabConfig(host=os.getenv("GITLAB_HOST", "gitlab.com"), token=os.getenv("GITLAB_TOKEN", "")) if not config.token: logger.error("GitLab token not set") raise ValueError("GitLab token not set. Please set GITLAB_TOKEN in your environment.") url = f"https://{config.host}/api/{config.api_version}/{endpoint}" headers = {"Accept": "application/json", "User-Agent": "GitLabMCPCodeReview/1.0", "Private-Token": config.token} try: if method.upper() == "GET": response = requests.get(url, headers=headers, verify=True, timeout=30) elif method.upper() == "POST": response = requests.post(url, headers=headers, json=data, verify=True, timeout=30) else: raise ValueError(f"Unsupported HTTP method: {method}") 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)}") async def fetch_merge_request(project_id: str, merge_request_iid: str) -> str: """ 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: JSON string containing the merge request information """ try: # Get merge request details mr_endpoint = f"projects/{quote(project_id, safe='')}/merge_requests/{merge_request_iid}" mr_info = make_gitlab_api_request(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(changes_endpoint) # Get the commit information commits_endpoint = f"{mr_endpoint}/commits" commits_info = make_gitlab_api_request(commits_endpoint) # Get the notes (comments) for this merge request notes_endpoint = f"{mr_endpoint}/notes" notes_info = make_gitlab_api_request(notes_endpoint) result = {"merge_request": mr_info, "changes": changes_info, "commits": commits_info, "notes": notes_info} return json.dumps(result, indent=2) except Exception as e: return f"Error fetching merge request: {str(e)}" async def fetch_merge_request_diff(project_id: str, merge_request_iid: str, file_path: Optional[str] = None) -> str: """ 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: JSON string containing the diff information """ try: # 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(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") result = {"merge_request_iid": merge_request_iid, "files": files} return json.dumps(result, indent=2) except Exception as e: return f"Error fetching merge request diff: {str(e)}" async def fetch_commit_diff(project_id: str, commit_sha: str, file_path: Optional[str] = None) -> str: """ 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: JSON string containing the diff information """ try: # 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(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(commit_endpoint) result = {"commit": commit_info, "diffs": diff_info} return json.dumps(result, indent=2) except Exception as e: return f"Error fetching commit diff: {str(e)}" async def compare_versions(project_id: str, from_sha: str, to_sha: str) -> str: """ 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: JSON string containing the comparison information """ try: # 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(compare_endpoint) if not compare_info: raise ValueError(f"Comparison failed between {from_sha} and {to_sha}") return json.dumps(compare_info, indent=2) except Exception as e: return f"Error comparing versions: {str(e)}" async def add_merge_request_comment( project_id: str, merge_request_iid: str, body: str, position: Optional[Dict[str, Any]] = None ) -> str: """ 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 (JSON string) Returns: JSON string containing the created comment information """ try: # Create the comment data data = {"body": body} # Parse position data if provided if position: if isinstance(position, str): data["position"] = json.loads(position) else: 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(comment_endpoint, method="POST", data=data) if not comment_info: raise ValueError("Failed to add comment to merge request") return json.dumps(comment_info, indent=2) except Exception as e: return f"Error adding merge request comment: {str(e)}" async def approve_merge_request( project_id: str, merge_request_iid: str, approvals_required: Optional[int] = None ) -> str: """ 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: JSON string containing the approval information """ try: # Approve the merge request approve_endpoint = f"projects/{quote(project_id, safe='')}/merge_requests/{merge_request_iid}/approve" approve_info = make_gitlab_api_request(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(approvals_endpoint, method="POST", data=data) return json.dumps(approve_info, indent=2) except Exception as e: return f"Error approving merge request: {str(e)}" async def get_project_merge_requests(project_id: str, state: str = "all", limit: int = 20) -> str: """ 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: JSON string containing list of merge request objects """ try: # 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(mrs_endpoint) return json.dumps(mrs_info, indent=2) except Exception as e: return f"Error fetching project merge requests: {str(e)}" async def get_gitlab_status() -> str: """Get GitLab connection status and configuration.""" config = GitLabConfig(host=os.getenv("GITLAB_HOST", "gitlab.com"), token=os.getenv("GITLAB_TOKEN", "")) status = {"host": config.host, "token_configured": bool(config.token), "api_version": config.api_version} if config.token: try: # Test connection user_endpoint = "user" user_info = make_gitlab_api_request(user_endpoint) status["connection"] = "success" status["user"] = { "username": user_info.get("username", ""), "name": user_info.get("name", ""), "id": user_info.get("id", ""), } except Exception as e: status["connection"] = "failed" status["error"] = str(e) return json.dumps(status, indent=2) async def main() -> None: """Run the GitLab MCP server.""" # Create server with GitLab configuration server = MCPServerHero(name="GitLab Code Review Server", debug=True) # Add rate limiting for API calls server.add_middleware( RateLimitMiddleware(tool_limit=30, resource_limit=50, prompt_limit=10, refill_rate=0.5, window_size=60) ) # Setup GitLab authentication auth_provider = GitLabAuthProvider() server.enable_auth_provider(auth_provider) # Register GitLab tools server.add_tool( name="fetch_merge_request", tool_func=fetch_merge_request, description="Fetch a GitLab merge request and its contents including changes, commits, and comments", schema={ "type": "object", "properties": { "project_id": { "type": "string", "description": "The GitLab project ID or URL-encoded path (e.g., 'group/project' or '12345')", }, "merge_request_iid": { "type": "string", "description": "The merge request IID (project-specific ID, not the global ID)", }, }, "required": ["project_id", "merge_request_iid"], }, ) server.add_tool( name="fetch_merge_request_diff", tool_func=fetch_merge_request_diff, description="Fetch the diff for files in a merge request, optionally filtered by specific file path", schema={ "type": "object", "properties": { "project_id": {"type": "string", "description": "The GitLab project ID or URL-encoded path"}, "merge_request_iid": {"type": "string", "description": "The merge request IID (project-specific ID)"}, "file_path": {"type": "string", "description": "Optional specific file path to get diff for"}, }, "required": ["project_id", "merge_request_iid"], }, ) server.add_tool( name="fetch_commit_diff", tool_func=fetch_commit_diff, description="Fetch the diff for a specific commit, optionally filtered by file path", schema={ "type": "object", "properties": { "project_id": {"type": "string", "description": "The GitLab project ID or URL-encoded path"}, "commit_sha": {"type": "string", "description": "The commit SHA"}, "file_path": {"type": "string", "description": "Optional specific file path to get diff for"}, }, "required": ["project_id", "commit_sha"], }, ) server.add_tool( name="compare_versions", tool_func=compare_versions, description="Compare two commits/branches/tags to see the differences between them", schema={ "type": "object", "properties": { "project_id": {"type": "string", "description": "The GitLab project ID or URL-encoded path"}, "from_sha": {"type": "string", "description": "The source commit/branch/tag"}, "to_sha": {"type": "string", "description": "The target commit/branch/tag"}, }, "required": ["project_id", "from_sha", "to_sha"], }, ) server.add_tool( name="add_merge_request_comment", tool_func=add_merge_request_comment, description="Add a comment to a merge request, optionally at a specific position in a file", schema={ "type": "object", "properties": { "project_id": {"type": "string", "description": "The GitLab project ID or URL-encoded path"}, "merge_request_iid": {"type": "string", "description": "The merge request IID (project-specific ID)"}, "body": {"type": "string", "description": "The comment text"}, "position": { "type": "string", "description": "Optional position data for line comments as JSON string", }, }, "required": ["project_id", "merge_request_iid", "body"], }, ) server.add_tool( name="approve_merge_request", tool_func=approve_merge_request, description="Approve a merge request and optionally set required approvals", schema={ "type": "object", "properties": { "project_id": {"type": "string", "description": "The GitLab project ID or URL-encoded path"}, "merge_request_iid": {"type": "string", "description": "The merge request IID (project-specific ID)"}, "approvals_required": { "type": "integer", "description": "Optional number of required approvals to set", }, }, "required": ["project_id", "merge_request_iid"], }, ) server.add_tool( name="get_project_merge_requests", tool_func=get_project_merge_requests, description="Get all merge requests for a project with optional filtering by state", schema={ "type": "object", "properties": { "project_id": {"type": "string", "description": "The GitLab project ID or URL-encoded path"}, "state": { "type": "string", "enum": ["all", "opened", "closed", "merged", "locked"], "description": "Filter merge requests by state", "default": "all", }, "limit": { "type": "integer", "description": "Maximum number of merge requests to return", "default": 20, "minimum": 1, "maximum": 100, }, }, "required": ["project_id"], }, ) # Register GitLab status resource server.add_resource( uri="gitlab://status", resource_func=get_gitlab_status, name="gitlab_status", description="GitLab connection status and configuration information", ) # Initialize server await server.initialize() print("🦊 GitLab Code Review MCP Server started with features:") print(" ✅ Middleware: Validation, Logging, Timing, Rate Limiting") print(" ✅ Authentication: GitLab token-based auth") print(" ✅ Tools: Merge Request operations, Diff analysis, Comments") print(" ✅ Resources: GitLab connection status") print(" 📋 Environment variables required:") print(" - GITLAB_TOKEN: Your GitLab personal access token") print(" - GITLAB_HOST: GitLab host (default: gitlab.com)") try: # Run the server async with stdio_server() as streams: await server.run(streams[0], streams[1], server.create_initialization_options()) finally: await server.shutdown() if __name__ == "__main__": asyncio.run(main())

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/namnd00/mcp-server-hero'

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