summaryrefslogtreecommitdiff
path: root/servers/gitlab_glab/src/mcp_server_gitlab_glab
diff options
context:
space:
mode:
authorDawid Rycerz <dawid@rycerz.xyz>2025-03-28 21:39:04 +0100
committerDawid Rycerz <dawid@rycerz.xyz>2025-03-28 21:39:04 +0100
commit903f0d9ca388533ab44615e414379fa5b305a7d1 (patch)
treed4225b3b07e11792d06660b31da97f786b5578e9 /servers/gitlab_glab/src/mcp_server_gitlab_glab
parent1745749cd2745c94c3f852e9c02dfde19d8d9c20 (diff)
Add basic glab mcp server
Diffstat (limited to 'servers/gitlab_glab/src/mcp_server_gitlab_glab')
-rw-r--r--servers/gitlab_glab/src/mcp_server_gitlab_glab/__init__.py7
-rw-r--r--servers/gitlab_glab/src/mcp_server_gitlab_glab/__main__.py7
-rw-r--r--servers/gitlab_glab/src/mcp_server_gitlab_glab/cli.py115
-rw-r--r--servers/gitlab_glab/src/mcp_server_gitlab_glab/server.py194
4 files changed, 323 insertions, 0 deletions
diff --git a/servers/gitlab_glab/src/mcp_server_gitlab_glab/__init__.py b/servers/gitlab_glab/src/mcp_server_gitlab_glab/__init__.py
new file mode 100644
index 0000000..cd431bb
--- /dev/null
+++ b/servers/gitlab_glab/src/mcp_server_gitlab_glab/__init__.py
@@ -0,0 +1,7 @@
+"""GitLab CLI MCP Server package.
+
+This package provides an MCP server that integrates with GitLab through the GitLab CLI
+(glab).
+"""
+
+__version__ = "0.1.0"
diff --git a/servers/gitlab_glab/src/mcp_server_gitlab_glab/__main__.py b/servers/gitlab_glab/src/mcp_server_gitlab_glab/__main__.py
new file mode 100644
index 0000000..dedd9f8
--- /dev/null
+++ b/servers/gitlab_glab/src/mcp_server_gitlab_glab/__main__.py
@@ -0,0 +1,7 @@
+#!/usr/bin/env python3
+"""Command-line interface for the GitLab CLI MCP server."""
+
+from .cli import run_server
+
+if __name__ == "__main__":
+ run_server()
diff --git a/servers/gitlab_glab/src/mcp_server_gitlab_glab/cli.py b/servers/gitlab_glab/src/mcp_server_gitlab_glab/cli.py
new file mode 100644
index 0000000..69173f7
--- /dev/null
+++ b/servers/gitlab_glab/src/mcp_server_gitlab_glab/cli.py
@@ -0,0 +1,115 @@
+#!/usr/bin/env python3
+"""Common CLI functionality for the GitLab CLI MCP server."""
+
+import argparse
+import asyncio
+import logging
+import os
+import sys
+
+from .server import main
+
+logger = logging.getLogger("mcp_gitlab_glab_server")
+
+
+def parse_args() -> argparse.Namespace:
+ """Parse command-line arguments.
+
+ Returns:
+ The parsed command-line arguments.
+ """
+ parser = argparse.ArgumentParser(description="GitLab CLI MCP Server")
+ parser.add_argument(
+ "--transport",
+ choices=["stdio", "remote"],
+ default="stdio",
+ help="Transport type (stdio or remote)",
+ )
+ parser.add_argument(
+ "--host",
+ default="127.0.0.1",
+ help="Host to bind to for remote transport (default: 127.0.0.1)",
+ )
+ parser.add_argument(
+ "--port",
+ type=int,
+ default=8080,
+ help="Port to bind to for remote transport (default: 8080)",
+ )
+ parser.add_argument(
+ "--log-level",
+ choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
+ default="INFO",
+ help="Set the logging level (default: INFO)",
+ )
+ return parser.parse_args()
+
+
+def validate_args(args: argparse.Namespace) -> argparse.Namespace:
+ """Validate command-line arguments.
+
+ Args:
+ args: The command-line arguments to validate.
+
+ Returns:
+ The validated command-line arguments.
+ """
+ if (
+ args.transport == "remote"
+ and args.port < 1024
+ and not sys.platform.startswith("win")
+ ):
+ logger.warning(
+ "Using a port below 1024 may require root privileges on Unix-like systems."
+ )
+ return args
+
+
+def setup_logging(level: str, transport: str) -> None:
+ """Set up logging configuration.
+
+ Args:
+ level: The base logging level from command line args.
+ transport: The transport type being used (stdio or remote).
+ """
+ # Create logs directory if it doesn't exist
+ os.makedirs("logs", exist_ok=True)
+
+ # Configure file handler for all logs
+ file_handler = logging.FileHandler("logs/mcp_server.log")
+ file_handler.setLevel(getattr(logging, level))
+ file_handler.setFormatter(
+ logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
+ )
+
+ # Configure console handler with appropriate level
+ console_handler = logging.StreamHandler()
+ # Use WARNING level for stdio transport to reduce interference
+ if transport == "stdio":
+ console_handler.setLevel(logging.WARNING)
+ else:
+ console_handler.setLevel(getattr(logging, level))
+ console_handler.setFormatter(
+ logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
+ )
+
+ # Configure root logger
+ root_logger = logging.getLogger()
+ root_logger.setLevel(getattr(logging, level)) # Base level for all handlers
+ root_logger.handlers = [] # Remove any existing handlers
+ root_logger.addHandler(file_handler)
+ root_logger.addHandler(console_handler)
+
+
+def run_server() -> None:
+ """Run the server with CLI arguments."""
+ args = validate_args(parse_args())
+ setup_logging(args.log_level, args.transport)
+
+ try:
+ asyncio.run(main(args.transport, args.host, args.port))
+ except KeyboardInterrupt:
+ logger.info("Server stopped by user")
+ except Exception as e:
+ logger.error(f"Error running server: {e}")
+ sys.exit(1)
diff --git a/servers/gitlab_glab/src/mcp_server_gitlab_glab/server.py b/servers/gitlab_glab/src/mcp_server_gitlab_glab/server.py
new file mode 100644
index 0000000..38a9eb6
--- /dev/null
+++ b/servers/gitlab_glab/src/mcp_server_gitlab_glab/server.py
@@ -0,0 +1,194 @@
+#!/usr/bin/env python3
+"""GitLab CLI MCP Server implementation.
+
+This module provides an MCP server that integrates with GitLab through the GitLab CLI
+(glab).
+"""
+
+import json
+import logging
+import os
+import subprocess
+import sys
+from typing import Any
+
+from mcp.server.fastmcp import FastMCP
+
+# reconfigure UnicodeEncodeError prone default (i.e. windows-1252) to utf-8
+if sys.platform == "win32" and os.environ.get("PYTHONIOENCODING") is None:
+ sys.stdin.reconfigure(encoding="utf-8")
+ sys.stdout.reconfigure(encoding="utf-8")
+ sys.stderr.reconfigure(encoding="utf-8")
+
+logger = logging.getLogger("mcp_gitlab_glab_server")
+
+
+class GitLabServer:
+ """GitLab server implementation using the GitLab CLI (glab)."""
+
+ def __init__(self) -> None:
+ """Initialize the GitLab server."""
+ self.auth_message = (
+ "Authentication required. Please run 'glab auth login' to authenticate."
+ )
+
+ def execute_glab_command(self, args: list[str]) -> tuple[bool, Any]:
+ """Execute a glab command and return the result.
+
+ Args:
+ args: List of command arguments to pass to glab.
+
+ Returns:
+ A tuple containing:
+ - A boolean indicating success (True) or failure (False)
+ - The command output (if successful) or an error message (if failed)
+ """
+ try:
+ result = subprocess.run(
+ ["glab"] + args,
+ capture_output=True,
+ text=True,
+ check=False,
+ )
+
+ if result.returncode != 0:
+ error_msg = result.stderr.strip()
+ logger.error(f"glab command failed: {error_msg}")
+
+ # Check for authentication errors
+ if "authentication required" in error_msg.lower():
+ return False, {"error": self.auth_message}
+
+ return False, {"error": error_msg}
+
+ # For API commands, parse JSON output
+ if args and args[0] == "api":
+ try:
+ return True, json.loads(result.stdout)
+ except json.JSONDecodeError:
+ logger.error("Failed to parse JSON response")
+ return False, {"error": "Failed to parse JSON response"}
+
+ return True, result.stdout.strip()
+ except FileNotFoundError:
+ logger.error("glab command not found")
+ return False, {
+ "error": "glab command not found. Please install GitLab CLI."
+ }
+ except Exception as e:
+ logger.error(f"Command execution failed: {str(e)}")
+ return False, {"error": f"Command execution failed: {str(e)}"}
+
+ def check_availability(self) -> dict[str, Any]:
+ """Check if the glab CLI tool is available and accessible.
+
+ Returns:
+ A dictionary containing availability status and version information.
+ """
+ success, result = self.execute_glab_command(["--version"])
+
+ if success:
+ return {
+ "available": True,
+ "version": result,
+ }
+ else:
+ return {
+ "available": False,
+ "error": result.get("error", "Unknown error"),
+ }
+
+ def find_project(self, project_name: str) -> dict[str, Any]:
+ """Find a GitLab project by name.
+
+ Args:
+ project_name: The name of the project to search for.
+
+ Returns:
+ A dictionary containing project information if found, or an error message.
+ """
+ success, result = self.execute_glab_command(
+ ["api", f"/projects?search={project_name}"]
+ )
+
+ if not success:
+ return result
+
+ # Check if any projects were found
+ if isinstance(result, list) and len(result) > 0:
+ # Return the first matching project
+ project = result[0]
+ return {
+ "id": project.get("id"),
+ "name": project.get("name"),
+ "path_with_namespace": project.get("path_with_namespace"),
+ "web_url": project.get("web_url"),
+ "description": project.get("description"),
+ }
+ else:
+ return {"error": f"Project '{project_name}' not found"}
+
+
+def create_server(host: str = "127.0.0.1", port: int = 8080) -> FastMCP:
+ """Create and configure the FastMCP server.
+
+ Args:
+ host: The host to bind to for remote transport.
+ port: The port to bind to for remote transport.
+
+ Returns:
+ A configured FastMCP server instance.
+ """
+ # Create a FastMCP server with host and port settings
+ mcp = FastMCP("GitLab CLI", host=host, port=port)
+
+ # Create a GitLabServer instance
+ gitlab = GitLabServer()
+
+ # Add check_glab_availability tool
+ @mcp.tool()
+ def check_glab_availability() -> dict[str, Any]:
+ """Check if the glab CLI tool is available and accessible.
+
+ Returns:
+ A dictionary containing availability status and version information.
+ """
+ return gitlab.check_availability()
+
+ # Add find_project tool
+ @mcp.tool()
+ def find_project(project_name: str) -> dict[str, Any]:
+ """Find a GitLab project by name and return its ID.
+
+ Args:
+ project_name: The name of the project to search for.
+
+ Returns:
+ A dictionary containing project information if found, or an error message.
+ """
+ return gitlab.find_project(project_name)
+
+ return mcp
+
+
+async def main(transport_type: str, host: str, port: int) -> None:
+ """Start the server with the specified transport.
+
+ Args:
+ transport_type: The transport type to use ("stdio" or "remote").
+ host: The host to bind to for remote transport.
+ port: The port to bind to for remote transport.
+ """
+ logger.info("Starting MCP GitLab CLI Server")
+ logger.info(f"Starting GitLab CLI MCP Server with {transport_type} transport")
+
+ # Create the server with host and port
+ mcp = create_server(host=host, port=port)
+
+ # Run the server with the appropriate transport
+ if transport_type == "stdio":
+ logger.info("Server running with stdio transport")
+ await mcp.run_stdio_async()
+ else: # remote transport
+ logger.info(f"Server running with remote transport on {host}:{port}")
+ await mcp.run_sse_async()