summaryrefslogtreecommitdiff
path: root/servers/gitlab_glab
diff options
context:
space:
mode:
Diffstat (limited to 'servers/gitlab_glab')
-rw-r--r--servers/gitlab_glab/README.md22
-rw-r--r--servers/gitlab_glab/src/mcp_server_gitlab_glab/server.py88
-rw-r--r--servers/gitlab_glab/tests/test_integration.py6
-rw-r--r--servers/gitlab_glab/tests/test_server.py155
4 files changed, 212 insertions, 59 deletions
diff --git a/servers/gitlab_glab/README.md b/servers/gitlab_glab/README.md
index 21f40cb..5a6fff1 100644
--- a/servers/gitlab_glab/README.md
+++ b/servers/gitlab_glab/README.md
@@ -50,24 +50,40 @@ Checks if the GitLab CLI tool is installed and accessible.
result = use_mcp_tool(
server_name="gitlab_glab",
tool_name="check_glab_availability",
- arguments={}
+ arguments={
+ "working_directory": "/path/to/current/directory"
+ }
)
```
### find_project
-Finds a GitLab project by name and returns its ID and other details.
+Finds GitLab projects by name and returns their details.
```python
result = use_mcp_tool(
server_name="gitlab_glab",
tool_name="find_project",
arguments={
- "project_name": "my-project"
+ "project_name": "my-project",
+ "working_directory": "/path/to/current/directory"
}
)
```
+The function returns a list of matching projects, each containing the following fields:
+- `id`: The project ID
+- `name`: The project name
+- `path_with_namespace`: The project path with namespace
+- `web_url`: The project web URL
+- `description`: The project description
+
+## 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.
+
+The working directory should be the absolute path to the directory where you want the GitLab CLI commands to be executed. For example, if you're working with a GitLab repository cloned to `/home/user/projects/my-repo`, you would pass that path as the `working_directory` parameter.
+
## Development
### Running tests
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 38a9eb6..e6c4d47 100644
--- a/servers/gitlab_glab/src/mcp_server_gitlab_glab/server.py
+++ b/servers/gitlab_glab/src/mcp_server_gitlab_glab/server.py
@@ -32,11 +32,14 @@ class GitLabServer:
"Authentication required. Please run 'glab auth login' to authenticate."
)
- def execute_glab_command(self, args: list[str]) -> tuple[bool, Any]:
+ def execute_glab_command(
+ self, args: list[str], working_directory: str
+ ) -> tuple[bool, Any]:
"""Execute a glab command and return the result.
Args:
args: List of command arguments to pass to glab.
+ working_directory: The directory to execute the command in.
Returns:
A tuple containing:
@@ -49,16 +52,17 @@ class GitLabServer:
capture_output=True,
text=True,
check=False,
+ cwd=working_directory,
)
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
@@ -79,14 +83,17 @@ class GitLabServer:
logger.error(f"Command execution failed: {str(e)}")
return False, {"error": f"Command execution failed: {str(e)}"}
- def check_availability(self) -> dict[str, Any]:
+ def check_availability(self, working_directory: str) -> dict[str, Any]:
"""Check if the glab CLI tool is available and accessible.
+ Args:
+ working_directory: The directory to execute the command in.
+
Returns:
A dictionary containing availability status and version information.
"""
- success, result = self.execute_glab_command(["--version"])
-
+ success, result = self.execute_glab_command(["--version"], working_directory)
+
if success:
return {
"available": True,
@@ -98,33 +105,41 @@ class GitLabServer:
"error": result.get("error", "Unknown error"),
}
- def find_project(self, project_name: str) -> dict[str, Any]:
- """Find a GitLab project by name.
+ def find_project(
+ self, project_name: str, working_directory: str
+ ) -> dict[str, Any] | list[dict[str, Any]]:
+ """Find GitLab projects by name.
Args:
project_name: The name of the project to search for.
+ working_directory: The directory to execute the command in.
Returns:
- A dictionary containing project information if found, or an error message.
+ A list of dictionaries containing project information if found,
+ or an error message.
"""
success, result = self.execute_glab_command(
- ["api", f"/projects?search={project_name}"]
+ ["api", f"/projects?search_namespaces=true&search={project_name}"], working_directory
)
-
+
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"),
- }
+ # Return all matching projects
+ projects = []
+ for project in result:
+ projects.append(
+ {
+ "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"),
+ }
+ )
+ return projects
else:
return {"error": f"Project '{project_name}' not found"}
@@ -141,33 +156,40 @@ def create_server(host: str = "127.0.0.1", port: int = 8080) -> FastMCP:
"""
# 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]:
+ def check_glab_availability(working_directory: str) -> dict[str, Any]:
"""Check if the glab CLI tool is available and accessible.
+ Args:
+ working_directory: The directory to execute the command in.
+
Returns:
A dictionary containing availability status and version information.
"""
- return gitlab.check_availability()
-
+ return gitlab.check_availability(working_directory)
+
# 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.
+ def find_project(
+ project_name: str, working_directory: str
+ ) -> dict[str, Any] | list[dict[str, Any]]:
+ """Find GitLab projects by name.
Args:
project_name: The name of the project to search for.
+ working_directory: The directory to execute the command in.
Returns:
- A dictionary containing project information if found, or an error message.
+ A list of dictionaries containing project information if found,
+ or an error message.
"""
- return gitlab.find_project(project_name)
-
+ return gitlab.find_project(project_name, working_directory)
+
return mcp
@@ -181,10 +203,10 @@ async def main(transport_type: str, host: str, port: int) -> None:
"""
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")
diff --git a/servers/gitlab_glab/tests/test_integration.py b/servers/gitlab_glab/tests/test_integration.py
index 3bad1cc..b25e715 100644
--- a/servers/gitlab_glab/tests/test_integration.py
+++ b/servers/gitlab_glab/tests/test_integration.py
@@ -24,6 +24,12 @@ class TestIntegration:
# Verify tools were registered
assert mock_server.tool.call_count == 2
+
+ # Verify that the tool decorator was called with functions that have
+ # working_directory parameter. We can't directly access the decorated functions
+ # in the mock, so we'll check indirectly by verifying that the server was
+ # created
+ assert mock_server is not None
@patch("mcp_server_gitlab_glab.server.FastMCP")
def test_create_server_custom_params(self, mock_fastmcp: MagicMock) -> None:
diff --git a/servers/gitlab_glab/tests/test_server.py b/servers/gitlab_glab/tests/test_server.py
index 1c27ea5..e612173 100644
--- a/servers/gitlab_glab/tests/test_server.py
+++ b/servers/gitlab_glab/tests/test_server.py
@@ -27,7 +27,8 @@ class TestGitLabServer:
mock_run.return_value = mock_process
server = GitLabServer()
- success, result = server.execute_glab_command(["--version"])
+ working_dir = "/test/directory"
+ success, result = server.execute_glab_command(["--version"], working_dir)
assert success is True
assert result == "command output"
@@ -36,6 +37,7 @@ class TestGitLabServer:
capture_output=True,
text=True,
check=False,
+ cwd=working_dir,
)
@patch("subprocess.run")
@@ -49,7 +51,8 @@ class TestGitLabServer:
mock_run.return_value = mock_process
server = GitLabServer()
- success, result = server.execute_glab_command(["--version"])
+ working_dir = "/test/directory"
+ success, result = server.execute_glab_command(["--version"], working_dir)
assert success is False
assert result == {"error": "command failed"}
@@ -58,6 +61,7 @@ class TestGitLabServer:
capture_output=True,
text=True,
check=False,
+ cwd=working_dir,
)
@patch("subprocess.run")
@@ -71,7 +75,8 @@ class TestGitLabServer:
mock_run.return_value = mock_process
server = GitLabServer()
- success, result = server.execute_glab_command(["api", "/projects"])
+ working_dir = "/test/directory"
+ success, result = server.execute_glab_command(["api", "/projects"], working_dir)
assert success is False
assert "error" in result
@@ -81,6 +86,7 @@ class TestGitLabServer:
capture_output=True,
text=True,
check=False,
+ cwd=working_dir,
)
@patch("subprocess.run")
@@ -90,7 +96,8 @@ class TestGitLabServer:
mock_run.side_effect = FileNotFoundError("No such file or directory: 'glab'")
server = GitLabServer()
- success, result = server.execute_glab_command(["--version"])
+ working_dir = "/test/directory"
+ success, result = server.execute_glab_command(["--version"], working_dir)
assert success is False
assert "error" in result
@@ -107,7 +114,8 @@ class TestGitLabServer:
mock_run.return_value = mock_process
server = GitLabServer()
- success, result = server.execute_glab_command(["api", "/projects"])
+ working_dir = "/test/directory"
+ success, result = server.execute_glab_command(["api", "/projects"], working_dir)
assert success is True
assert isinstance(result, list)
@@ -119,6 +127,7 @@ class TestGitLabServer:
capture_output=True,
text=True,
check=False,
+ cwd=working_dir,
)
@patch("subprocess.run")
@@ -132,7 +141,8 @@ class TestGitLabServer:
mock_run.return_value = mock_process
server = GitLabServer()
- success, result = server.execute_glab_command(["api", "/projects"])
+ working_dir = "/test/directory"
+ success, result = server.execute_glab_command(["api", "/projects"], working_dir)
assert success is False
assert "error" in result
@@ -145,11 +155,12 @@ class TestGitLabServer:
mock_execute.return_value = (True, "glab version 1.0.0")
server = GitLabServer()
- result = server.check_availability()
+ working_dir = "/test/directory"
+ result = server.check_availability(working_dir)
assert result["available"] is True
assert result["version"] == "glab version 1.0.0"
- mock_execute.assert_called_once_with(["--version"])
+ mock_execute.assert_called_once_with(["--version"], working_dir)
@patch.object(GitLabServer, "execute_glab_command")
def test_check_availability_failure(self, mock_execute: MagicMock) -> None:
@@ -158,15 +169,16 @@ class TestGitLabServer:
mock_execute.return_value = (False, {"error": "glab command not found"})
server = GitLabServer()
- result = server.check_availability()
+ working_dir = "/test/directory"
+ result = server.check_availability(working_dir)
assert result["available"] is False
assert result["error"] == "glab command not found"
- mock_execute.assert_called_once_with(["--version"])
+ mock_execute.assert_called_once_with(["--version"], working_dir)
@patch.object(GitLabServer, "execute_glab_command")
def test_find_project_success(self, mock_execute: MagicMock) -> None:
- """Test successful find_project."""
+ """Test successful find_project with a single project."""
# Mock successful API response with a project
mock_execute.return_value = (
True,
@@ -182,15 +194,76 @@ class TestGitLabServer:
)
server = GitLabServer()
- result = server.find_project("test-project")
+ working_dir = "/test/directory"
+ result = server.find_project("test-project", working_dir)
- assert "id" in result
- assert result["id"] == 1
- assert result["name"] == "test-project"
- assert result["path_with_namespace"] == "group/test-project"
- assert result["web_url"] == "https://gitlab.com/group/test-project"
- assert result["description"] == "A test project"
- mock_execute.assert_called_once_with(["api", "/projects?search=test-project"])
+ assert isinstance(result, list)
+ assert len(result) == 1
+ assert result[0]["id"] == 1
+ assert result[0]["name"] == "test-project"
+ assert result[0]["path_with_namespace"] == "group/test-project"
+ assert result[0]["web_url"] == "https://gitlab.com/group/test-project"
+ assert result[0]["description"] == "A test project"
+ mock_execute.assert_called_once_with(
+ ["api", "/projects?search=test-project"], working_dir
+ )
+
+ @patch.object(GitLabServer, "execute_glab_command")
+ def test_find_project_multiple_results(self, mock_execute: MagicMock) -> None:
+ """Test successful find_project with multiple projects."""
+ # Mock successful API response with multiple projects
+ mock_execute.return_value = (
+ True,
+ [
+ {
+ "id": 1,
+ "name": "test-project",
+ "path_with_namespace": "group/test-project",
+ "web_url": "https://gitlab.com/group/test-project",
+ "description": "A test project",
+ },
+ {
+ "id": 2,
+ "name": "test-project-2",
+ "path_with_namespace": "group/test-project-2",
+ "web_url": "https://gitlab.com/group/test-project-2",
+ "description": "Another test project",
+ },
+ {
+ "id": 3,
+ "name": "test-project-3",
+ "path_with_namespace": "group/test-project-3",
+ "web_url": "https://gitlab.com/group/test-project-3",
+ "description": "Yet another test project",
+ },
+ ],
+ )
+
+ server = GitLabServer()
+ working_dir = "/test/directory"
+ result = server.find_project("test-project", working_dir)
+
+ assert isinstance(result, list)
+ assert len(result) == 3
+
+ # Check first project
+ assert result[0]["id"] == 1
+ assert result[0]["name"] == "test-project"
+ assert result[0]["path_with_namespace"] == "group/test-project"
+
+ # Check second project
+ assert result[1]["id"] == 2
+ assert result[1]["name"] == "test-project-2"
+ assert result[1]["path_with_namespace"] == "group/test-project-2"
+
+ # Check third project
+ assert result[2]["id"] == 3
+ assert result[2]["name"] == "test-project-3"
+ assert result[2]["path_with_namespace"] == "group/test-project-3"
+
+ mock_execute.assert_called_once_with(
+ ["api", "/projects?search=test-project"], working_dir
+ )
@patch.object(GitLabServer, "execute_glab_command")
def test_find_project_not_found(self, mock_execute: MagicMock) -> None:
@@ -199,12 +272,13 @@ class TestGitLabServer:
mock_execute.return_value = (True, [])
server = GitLabServer()
- result = server.find_project("nonexistent-project")
+ working_dir = "/test/directory"
+ result = server.find_project("nonexistent-project", working_dir)
assert "error" in result
assert "not found" in result["error"]
mock_execute.assert_called_once_with(
- ["api", "/projects?search=nonexistent-project"]
+ ["api", "/projects?search=nonexistent-project"], working_dir
)
@patch.object(GitLabServer, "execute_glab_command")
@@ -214,8 +288,43 @@ class TestGitLabServer:
mock_execute.return_value = (False, {"error": "API error"})
server = GitLabServer()
- result = server.find_project("test-project")
+ working_dir = "/test/directory"
+ result = server.find_project("test-project", working_dir)
assert "error" in result
assert result["error"] == "API error"
- mock_execute.assert_called_once_with(["api", "/projects?search=test-project"])
+ mock_execute.assert_called_once_with(
+ ["api", "/projects?search=test-project"], working_dir
+ )
+
+ @patch("subprocess.run")
+ def test_working_directory_is_used(self, mock_run: MagicMock) -> None:
+ """Test that the working directory is correctly passed to subprocess.run."""
+ # Mock successful command execution
+ mock_process = MagicMock()
+ mock_process.returncode = 0
+ mock_process.stdout = "command output"
+ mock_process.stderr = ""
+ mock_run.return_value = mock_process
+
+ server = GitLabServer()
+
+ # Test with different working directories
+ working_dirs = [
+ "/home/user/project",
+ "/tmp/gitlab",
+ "/var/www/html",
+ ]
+
+ for working_dir in working_dirs:
+ server.execute_glab_command(["status"], working_dir)
+ mock_run.assert_called_with(
+ ["glab", "status"],
+ capture_output=True,
+ text=True,
+ check=False,
+ cwd=working_dir,
+ )
+
+ # Verify the number of calls
+ assert mock_run.call_count == len(working_dirs)