diff options
Diffstat (limited to 'servers/gitlab_glab')
| -rw-r--r-- | servers/gitlab_glab/README.md | 34 | ||||
| -rw-r--r-- | servers/gitlab_glab/src/mcp_server_gitlab_glab/server.py | 126 | ||||
| -rw-r--r-- | servers/gitlab_glab/tests/test_integration.py | 3 | ||||
| -rw-r--r-- | servers/gitlab_glab/tests/test_server.py | 177 |
4 files changed, 339 insertions, 1 deletions
diff --git a/servers/gitlab_glab/README.md b/servers/gitlab_glab/README.md index 50926fb..a2bb677 100644 --- a/servers/gitlab_glab/README.md +++ b/servers/gitlab_glab/README.md @@ -6,6 +6,9 @@ This MCP server provides integration with GitLab through the GitLab CLI (`glab`) - Check if the GitLab CLI is available and accessible - Find GitLab projects by name and retrieve their IDs +- Search for GitLab issues with various filters +- Create new GitLab issues +- Get merge request diffs with automatic handling of large diffs - More features to be added in the future ## Prerequisites @@ -145,6 +148,37 @@ The function returns a dictionary containing: - `url`: The URL of the created issue - `error`: Error message if the operation failed +### get_mr_diff + +Get the diff for a merge request using `glab mr diff`. + +```python +result = use_mcp_tool( + server_name="gitlab_glab", + tool_name="get_mr_diff", + arguments={ + "working_directory": "/path/to/current/directory", + # Optional parameters + "mr_id": "123", # MR ID or branch name (if None, uses current branch) + "color": "never", # Use color in diff output: always, never, auto (default: never) + "raw": True, # Use raw diff format (default: False) + "repo": "group/project", # Project path with namespace + "max_size_kb": 100 # Maximum size in KB before saving to temp file (default: 100) + } +) +``` + +The function returns a dictionary containing: +- `diff`: The diff content (if small enough) +- `size_kb`: The size of the diff in KB +- `temp_file_path`: Path to temporary file if diff is too large (None otherwise) +- `diff_too_large`: Boolean indicating if diff was saved to temp file +- `max_size_kb`: The configured maximum size threshold +- `message`: Human-readable message about temp file creation (if applicable) +- `error`: Error message if the operation failed + +**Note on Large Diffs**: To prevent overwhelming LLMs with extremely large diffs, this tool automatically saves diffs larger than `max_size_kb` (default: 100KB) to a temporary file and returns the file path instead of the content. This allows you to process large merge request diffs without hitting token limits. + ## Working Directory Context All tools require a `working_directory` parameter that specifies the directory context in which the GitLab CLI commands should be executed. This ensures that commands are run in the same directory as the agent, maintaining proper context for repository operations. 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..881228f 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,6 +310,98 @@ class GitLabServer: logger.error(f"Failed to extract issue URL from output: {result}") return {"error": "Failed to extract issue URL from command output"} + 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, + ) -> 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). + + Returns: + A dictionary containing the diff content or a path to temporary file + if too large. + """ + # 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 + + # Check if the diff is too large + diff_content = result + 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 create_server(host: str = "127.0.0.1", port: int = 8080) -> FastMCP: """Create and configure the FastMCP server. @@ -455,6 +548,39 @@ 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, + ) -> 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). + + 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, + ) + return mcp diff --git a/servers/gitlab_glab/tests/test_integration.py b/servers/gitlab_glab/tests/test_integration.py index 5d7eed5..b7d1005 100644 --- a/servers/gitlab_glab/tests/test_integration.py +++ b/servers/gitlab_glab/tests/test_integration.py @@ -28,7 +28,8 @@ class TestIntegration: # - find_project # - search_issues # - create_issue - assert mock_server.tool.call_count == 4 + # - get_mr_diff + assert mock_server.tool.call_count == 5 # Verify that the tool decorator was called with functions that have # working_directory parameter. We can't directly access the decorated functions diff --git a/servers/gitlab_glab/tests/test_server.py b/servers/gitlab_glab/tests/test_server.py index ab93026..a7e74a4 100644 --- a/servers/gitlab_glab/tests/test_server.py +++ b/servers/gitlab_glab/tests/test_server.py @@ -585,3 +585,180 @@ class TestGitLabServer: # Verify the number of calls assert mock_run.call_count == len(working_dirs) + + @patch.object(GitLabServer, "execute_glab_command") + def test_get_mr_diff_success_small(self, mock_execute: MagicMock) -> None: + """Test successful MR diff retrieval with small diff.""" + # Mock successful command execution with small diff + diff_content = """diff --git a/file.txt b/file.txt +index 1234567..abcdefg 100644 +--- a/file.txt ++++ b/file.txt +@@ -1,3 +1,4 @@ + line 1 + line 2 ++new line + line 3""" + mock_execute.return_value = (True, diff_content) + + server = GitLabServer() + working_dir = "/test/directory" + result = server.get_mr_diff( + working_directory=working_dir, + mr_id="123", + color="never", + raw=False, + repo="group/project", + ) + + assert "diff" in result + assert result["diff"] == diff_content + assert result["size_kb"] < 1 + assert result["temp_file_path"] is None + + # Verify command was called with correct arguments + mock_execute.assert_called_once_with( + ["mr", "diff", "123", "--color", "never", "-R", "group/project"], + working_dir, + ) + + @patch.object(GitLabServer, "execute_glab_command") + def test_get_mr_diff_success_current_branch(self, mock_execute: MagicMock) -> None: + """Test successful MR diff retrieval for current branch.""" + diff_content = "diff content" + mock_execute.return_value = (True, diff_content) + + server = GitLabServer() + working_dir = "/test/directory" + result = server.get_mr_diff(working_directory=working_dir) + + assert "diff" in result + assert result["diff"] == diff_content + assert result["temp_file_path"] is None + + # Verify command was called without MR ID + mock_execute.assert_called_once_with( + ["mr", "diff", "--color", "never"], + working_dir, + ) + + @patch.object(GitLabServer, "execute_glab_command") + def test_get_mr_diff_with_raw_option(self, mock_execute: MagicMock) -> None: + """Test MR diff retrieval with raw option.""" + diff_content = "raw diff content" + mock_execute.return_value = (True, diff_content) + + server = GitLabServer() + working_dir = "/test/directory" + result = server.get_mr_diff( + working_directory=working_dir, + mr_id="branch-name", + color="auto", + raw=True, + ) + + assert "diff" in result + assert result["diff"] == diff_content + + # Verify command was called with raw option + mock_execute.assert_called_once_with( + ["mr", "diff", "branch-name", "--color", "auto", "--raw"], + working_dir, + ) + + @patch("tempfile.NamedTemporaryFile") + @patch.object(GitLabServer, "execute_glab_command") + def test_get_mr_diff_large_diff_temp_file( + self, mock_execute: MagicMock, mock_temp_file: MagicMock + ) -> None: + """Test MR diff retrieval with large diff that gets saved to temp file.""" + # Create a large diff content (over 100KB) + large_diff = "x" * (101 * 1024) # 101 KB + mock_execute.return_value = (True, large_diff) + + # Mock temporary file + mock_file = MagicMock() + mock_file.name = "/tmp/mr_diff_12345.diff" + mock_temp_file.return_value.__enter__.return_value = mock_file + + server = GitLabServer() + working_dir = "/test/directory" + result = server.get_mr_diff( + working_directory=working_dir, + mr_id="123", + max_size_kb=100, + ) + + assert result["diff_too_large"] is True + assert result["size_kb"] > 100 + assert result["max_size_kb"] == 100 + assert result["temp_file_path"] == "/tmp/mr_diff_12345.diff" + assert "message" in result + + # Verify temp file was created with correct parameters + mock_temp_file.assert_called_once_with( + mode='w', + suffix='.diff', + prefix='mr_diff_', + delete=False, + encoding='utf-8' + ) + mock_file.write.assert_called_once_with(large_diff) + + @patch("tempfile.NamedTemporaryFile") + @patch.object(GitLabServer, "execute_glab_command") + def test_get_mr_diff_large_diff_temp_file_error( + self, mock_execute: MagicMock, mock_temp_file: MagicMock + ) -> None: + """Test MR diff retrieval with large diff and temp file creation error.""" + # Create a large diff content + large_diff = "x" * (101 * 1024) # 101 KB + mock_execute.return_value = (True, large_diff) + + # Mock temporary file creation error + mock_temp_file.side_effect = Exception("Permission denied") + + server = GitLabServer() + working_dir = "/test/directory" + result = server.get_mr_diff( + working_directory=working_dir, + mr_id="123", + max_size_kb=100, + ) + + assert "error" in result + assert "too large" in result["error"] + assert "Permission denied" in result["error"] + + @patch.object(GitLabServer, "execute_glab_command") + def test_get_mr_diff_command_failure(self, mock_execute: MagicMock) -> None: + """Test MR diff retrieval with command failure.""" + mock_execute.return_value = (False, {"error": "MR not found"}) + + server = GitLabServer() + working_dir = "/test/directory" + result = server.get_mr_diff(working_directory=working_dir, mr_id="999") + + assert "error" in result + assert result["error"] == "MR not found" + + @patch.object(GitLabServer, "execute_glab_command") + def test_get_mr_diff_invalid_color_option(self, mock_execute: MagicMock) -> None: + """Test MR diff with invalid color option skips color parameter.""" + diff_content = "diff content" + mock_execute.return_value = (True, diff_content) + + server = GitLabServer() + working_dir = "/test/directory" + result = server.get_mr_diff( + working_directory=working_dir, + mr_id="123", + color="invalid", # Invalid color option + ) + + assert "diff" in result + # Invalid color options should be filtered out, only valid ones are added + mock_execute.assert_called_once_with( + ["mr", "diff", "123"], + working_dir, + ) |
