summaryrefslogtreecommitdiff
path: root/servers/gitlab_glab/src
diff options
context:
space:
mode:
Diffstat (limited to 'servers/gitlab_glab/src')
-rw-r--r--servers/gitlab_glab/src/mcp_server_gitlab_glab/server.py340
1 files changed, 340 insertions, 0 deletions
diff --git a/servers/gitlab_glab/src/mcp_server_gitlab_glab/server.py b/servers/gitlab_glab/src/mcp_server_gitlab_glab/server.py
index d3fd074..f239246 100644
--- a/servers/gitlab_glab/src/mcp_server_gitlab_glab/server.py
+++ b/servers/gitlab_glab/src/mcp_server_gitlab_glab/server.py
@@ -11,6 +11,7 @@ import os
import re
import subprocess
import sys
+import tempfile
from typing import Any
from mcp.server.fastmcp import FastMCP
@@ -309,7 +310,273 @@ class GitLabServer:
logger.error(f"Failed to extract issue URL from output: {result}")
return {"error": "Failed to extract issue URL from command output"}
+ def filter_diff_content(
+ self,
+ diff_content: str,
+ exclude_extensions: list[str],
+ ) -> str:
+ """Filter out files with specified extensions from diff content.
+
+ Args:
+ diff_content: The original diff content.
+ exclude_extensions: List of file extensions to exclude
+ (e.g., [".lock", ".log"]).
+
+ Returns:
+ Filtered diff content with excluded files removed.
+ """
+ if not exclude_extensions:
+ return diff_content
+
+ lines = diff_content.split('\n')
+ filtered_lines = []
+ skip_block = False
+
+ for line in lines:
+ if line.startswith("diff --git a/"):
+ parts = line.split()
+ if len(parts) >= 4:
+ file_a = parts[2]
+ file_b = parts[3]
+ # Check if either file should be excluded
+ should_exclude = False
+ for ext in exclude_extensions:
+ # Handle both exact extension matches and lock file patterns
+ if ext == ".lock":
+ # Special handling for lock files
+ # (package-lock.json, yarn.lock, etc.)
+ if (file_a.endswith('.lock') or
+ file_b.endswith('.lock') or
+ 'lock.' in file_a or 'lock.' in file_b or
+ file_a.endswith('-lock.json') or
+ file_b.endswith('-lock.json')):
+ should_exclude = True
+ break
+ else:
+ # Standard extension matching
+ if file_a.endswith(ext) or file_b.endswith(ext):
+ should_exclude = True
+ break
+
+ skip_block = should_exclude
+ else:
+ skip_block = False
+
+ if not skip_block:
+ filtered_lines.append(line)
+
+ return '\n'.join(filtered_lines)
+
+ def get_mr_diff(
+ self,
+ working_directory: str,
+ mr_id: str | None = None,
+ color: str = "never",
+ raw: bool = False,
+ repo: str | None = None,
+ max_size_kb: int = 100,
+ filter_extensions: list[str] | None = None,
+ ) -> dict[str, Any]:
+ """Get the diff for a merge request.
+
+ Args:
+ working_directory: The directory to execute the command in.
+ mr_id: The merge request ID or branch name. If None, uses current branch.
+ color: Use color in diff output: always, never, auto (default: never).
+ raw: Use raw diff format that can be piped to commands.
+ repo: Select another repository (OWNER/REPO or GROUP/NAMESPACE/REPO format).
+ max_size_kb: Maximum size in KB before saving to temporary file
+ (default: 100).
+ filter_extensions: List of file extensions to exclude from diff
+ (default: [".lock", ".log"]).
+
+ Returns:
+ A dictionary containing the diff content or a path to temporary file
+ if too large.
+ """
+ # Set default filter extensions if not provided
+ if filter_extensions is None:
+ filter_extensions = [".lock", ".log"]
+
+ # Build command arguments
+ args = ["mr", "diff"]
+
+ # Add MR ID or branch if specified
+ if mr_id:
+ args.append(mr_id)
+
+ # Add color option
+ if color in ["always", "never", "auto"]:
+ args.extend(["--color", color])
+
+ # Add raw option
+ if raw:
+ args.append("--raw")
+
+ # Add repo option
+ if repo:
+ args.extend(["-R", repo])
+
+ # Execute the command
+ success, result = self.execute_glab_command(args, working_directory)
+
+ if not success:
+ return result
+
+ # Apply filtering to remove unwanted file extensions
+ diff_content = self.filter_diff_content(result, filter_extensions)
+
+ # Check if the diff is too large
+ diff_size_kb = len(diff_content.encode('utf-8')) / 1024
+
+ if diff_size_kb > max_size_kb:
+ try:
+ # Create a temporary file to store the large diff
+ with tempfile.NamedTemporaryFile(
+ mode='w',
+ suffix='.diff',
+ prefix='mr_diff_',
+ delete=False,
+ encoding='utf-8'
+ ) as temp_file:
+ temp_file.write(diff_content)
+ temp_path = temp_file.name
+
+ return {
+ "diff_too_large": True,
+ "size_kb": round(diff_size_kb, 2),
+ "max_size_kb": max_size_kb,
+ "temp_file_path": temp_path,
+ "message": (
+ f"Diff is too large ({diff_size_kb:.2f} KB > "
+ f"{max_size_kb} KB). Content saved to temporary file: "
+ f"{temp_path}"
+ )
+ }
+ except Exception as e:
+ logger.error(f"Failed to create temporary file: {str(e)}")
+ return {
+ "error": (
+ f"Diff is too large ({diff_size_kb:.2f} KB) and failed to "
+ f"create temporary file: {str(e)}"
+ )
+ }
+
+ return {
+ "diff": diff_content,
+ "size_kb": round(diff_size_kb, 2),
+ "temp_file_path": None
+ }
+ def run_ci_pipeline(
+ self,
+ working_directory: str,
+ branch: str | None = None,
+ variables: list[str] | None = None,
+ variables_env: list[str] | None = None,
+ variables_file: list[str] | None = None,
+ variables_from: str | None = None,
+ web_mode: bool = False,
+ repo: str | None = None,
+ ) -> dict[str, Any]:
+ """Run a CI/CD pipeline on GitLab.
+
+ Args:
+ working_directory: The directory to execute the command in.
+ branch: Create pipeline on branch/ref. If None, uses current branch.
+ variables: Pass variables to pipeline in format key:value.
+ variables_env: Pass environment variables to pipeline in format key:value.
+ variables_file: Pass file contents as file variables in format key:filename.
+ variables_from: JSON file containing variables for pipeline execution.
+ web_mode: Run pipeline in web mode (overrides CI_PIPELINE_SOURCE to 'web').
+ repo: Select another repository (OWNER/REPO or GROUP/NAMESPACE/REPO format).
+
+ Returns:
+ A dictionary containing the pipeline information or an error message.
+ """
+ # Build command arguments
+ args = ["ci", "run"]
+
+ # Handle branch - if not provided, get current branch
+ if branch is None:
+ try:
+ # Get current branch using git command
+ result = subprocess.run(
+ ["git", "branch", "--show-current"],
+ capture_output=True,
+ text=True,
+ check=False,
+ cwd=working_directory,
+ )
+ if result.returncode == 0 and result.stdout.strip():
+ branch = result.stdout.strip()
+ logger.info(f"Using current branch: {branch}")
+ else:
+ logger.warning(
+ "Could not determine current branch, proceeding without -b flag"
+ )
+ except Exception as e:
+ logger.warning(f"Could not determine current branch: {str(e)}")
+
+ # Add branch if available
+ if branch:
+ args.extend(["-b", branch])
+
+ # Add variables
+ if variables:
+ for var in variables:
+ args.extend(["--variables", var])
+
+ # Add environment variables
+ if variables_env:
+ for var in variables_env:
+ args.extend(["--variables-env", var])
+
+ # Add file variables
+ if variables_file:
+ for var in variables_file:
+ args.extend(["--variables-file", var])
+
+ # Add variables from file
+ if variables_from:
+ args.extend(["-f", variables_from])
+
+ # Add web mode - override CI_PIPELINE_SOURCE to 'web'
+ if web_mode:
+ args.extend(["--variables-env", "CI_PIPELINE_SOURCE:web"])
+
+ # Add repo
+ if repo:
+ args.extend(["-R", repo])
+
+ # Execute the command
+ success, result = self.execute_glab_command(args, working_directory)
+
+ if not success:
+ return result
+
+ # Parse the output to extract pipeline information
+ # The output typically contains pipeline URL and ID
+ output_lines = result.strip().split('\n') if isinstance(result, str) else []
+
+ pipeline_info = {
+ "success": True,
+ "output": result,
+ "branch": branch,
+ "web_mode": web_mode
+ }
+
+ # Try to extract pipeline URL if present
+ for line in output_lines:
+ if "https://" in line and "/pipelines/" in line:
+ # Extract just the URL from the line
+ import re
+ url_match = re.search(r'https://[^\s]+/pipelines/\d+', line)
+ if url_match:
+ pipeline_info["pipeline_url"] = url_match.group(0)
+ break
+
+ return pipeline_info
def create_server(host: str = "127.0.0.1", port: int = 8080) -> FastMCP:
"""Create and configure the FastMCP server.
@@ -455,6 +722,79 @@ def create_server(host: str = "127.0.0.1", port: int = 8080) -> FastMCP:
project=project,
)
+ @mcp.tool()
+ def get_mr_diff(
+ working_directory: str,
+ mr_id: str | None = None,
+ color: str = "never",
+ raw: bool = False,
+ repo: str | None = None,
+ max_size_kb: int = 100,
+ filter_extensions: list[str] | None = None,
+ ) -> dict[str, Any]:
+ """Get the diff for a merge request.
+
+ Args:
+ working_directory: The directory to execute the command in.
+ mr_id: The merge request ID or branch name. If None, uses current branch.
+ color: Use color in diff output: always, never, auto (default: never).
+ raw: Use raw diff format that can be piped to commands.
+ repo: Select another repository (OWNER/REPO or GROUP/NAMESPACE/REPO format).
+ max_size_kb: Maximum size in KB before saving to temporary file
+ (default: 100).
+ filter_extensions: List of file extensions to exclude from diff
+ (default: [".lock", ".log"]).
+
+ Returns:
+ A dictionary containing the diff content or a path to temporary file
+ if too large.
+ """
+ return gitlab.get_mr_diff(
+ working_directory=working_directory,
+ mr_id=mr_id,
+ color=color,
+ raw=raw,
+ repo=repo,
+ max_size_kb=max_size_kb,
+ filter_extensions=filter_extensions,
+ )
+
+ @mcp.tool()
+ def run_ci_pipeline(
+ working_directory: str,
+ branch: str | None = None,
+ variables: list[str] | None = None,
+ variables_env: list[str] | None = None,
+ variables_file: list[str] | None = None,
+ variables_from: str | None = None,
+ web_mode: bool = False,
+ repo: str | None = None,
+ ) -> dict[str, Any]:
+ """Run a CI/CD pipeline on GitLab.
+
+ Args:
+ working_directory: The directory to execute the command in.
+ branch: Create pipeline on branch/ref. If None, uses current branch.
+ variables: Pass variables to pipeline in format key:value.
+ variables_env: Pass environment variables to pipeline in format key:value.
+ variables_file: Pass file contents as file variables in format key:filename.
+ variables_from: JSON file containing variables for pipeline execution.
+ web_mode: Run pipeline in web mode (overrides CI_PIPELINE_SOURCE to 'web').
+ repo: Select another repository (OWNER/REPO or GROUP/NAMESPACE/REPO format).
+
+ Returns:
+ A dictionary containing the pipeline information or an error message.
+ """
+ return gitlab.run_ci_pipeline(
+ working_directory=working_directory,
+ branch=branch,
+ variables=variables,
+ variables_env=variables_env,
+ variables_file=variables_file,
+ variables_from=variables_from,
+ web_mode=web_mode,
+ repo=repo,
+ )
return mcp