summaryrefslogtreecommitdiff
path: root/servers/gitlab_glab
diff options
context:
space:
mode:
authorDawid Rycerz <dawid@rycerz.xyz>2025-03-31 23:38:35 +0200
committerDawid Rycerz <dawid@rycerz.xyz>2025-03-31 23:38:35 +0200
commitfa91e3fcbdd3b1d70f01de256e1c48fe7726ebd4 (patch)
tree5a76ae36e6fa37af9fb4a9879e6db9aa375c2654 /servers/gitlab_glab
parent73ee5037001cee54f6364f41e1011099308a15a3 (diff)
Add create_issue tool action
Diffstat (limited to 'servers/gitlab_glab')
-rw-r--r--servers/gitlab_glab/README.md26
-rw-r--r--servers/gitlab_glab/src/mcp_server_gitlab_glab/server.py97
-rw-r--r--servers/gitlab_glab/tests/test_integration.py2
-rw-r--r--servers/gitlab_glab/tests/test_server.py113
4 files changed, 233 insertions, 5 deletions
diff --git a/servers/gitlab_glab/README.md b/servers/gitlab_glab/README.md
index 5a6fff1..55ac6bb 100644
--- a/servers/gitlab_glab/README.md
+++ b/servers/gitlab_glab/README.md
@@ -78,6 +78,32 @@ The function returns a list of matching projects, each containing the following
- `web_url`: The project web URL
- `description`: The project description
+### create_issue
+
+Creates a new GitLab issue and returns its URL.
+
+```python
+result = use_mcp_tool(
+ server_name="gitlab_glab",
+ tool_name="create_issue",
+ arguments={
+ "title": "Issue title",
+ "description": "Issue description",
+ "working_directory": "/path/to/current/directory",
+ # Optional parameters
+ "labels": ["bug", "critical"], # List of labels
+ "assignee": ["username1", "username2"], # List of usernames
+ "milestone": "v1.0", # Milestone title or ID
+ "epic_id": 123, # Epic ID
+ "project": "group/project" # Project path with namespace
+ }
+)
+```
+
+The function returns a dictionary containing:
+- `url`: The URL of the created issue
+- `error`: Error message if the operation failed
+
## 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 e6c4d47..958e0a7 100644
--- a/servers/gitlab_glab/src/mcp_server_gitlab_glab/server.py
+++ b/servers/gitlab_glab/src/mcp_server_gitlab_glab/server.py
@@ -8,6 +8,7 @@ This module provides an MCP server that integrates with GitLab through the GitLa
import json
import logging
import os
+import re
import subprocess
import sys
from typing import Any
@@ -143,6 +144,64 @@ class GitLabServer:
else:
return {"error": f"Project '{project_name}' not found"}
+ def create_issue(
+ self,
+ title: str,
+ description: str,
+ working_directory: str,
+ labels: list[str] | None = None,
+ assignee: list[str] | None = None,
+ milestone: str | None = None,
+ epic_id: int | None = None,
+ project: str | None = None,
+ ) -> dict[str, Any]:
+ """Create a new GitLab issue.
+
+ Args:
+ title: The issue title.
+ description: The issue description.
+ labels: Optional list of labels to apply to the issue.
+ assignee: Optional list of usernames to assign the issue to.
+ milestone: Optional milestone title or ID.
+ epic_id: Optional ID of the epic to add the issue to.
+ project: Optional project name or path.
+ working_directory: The directory to execute the command in.
+
+ Returns:
+ A dictionary containing the issue URL or an error message.
+ """
+ # Build command arguments
+ args = ["issue", "create", "-y", "-t", title, "-d", description]
+
+ # Add optional arguments if provided
+ if labels:
+ args.extend(["-l", ",".join(labels)])
+ if assignee:
+ for user in assignee:
+ args.extend(["-a", user])
+ if milestone:
+ args.extend(["-m", milestone])
+ if epic_id:
+ args.extend(["--epic", str(epic_id)])
+ if project:
+ args.extend(["-R", project])
+
+ # Execute the command
+ success, result = self.execute_glab_command(args, working_directory)
+
+ if not success:
+ return result
+
+ # Parse the output to extract the issue URL
+ url_pattern = re.compile(r'https?://[^\s]+/issues/\d+')
+ match = url_pattern.search(result)
+ if match:
+ return {"url": match.group(0)}
+ else:
+ # Log the full output for debugging
+ logger.error(f"Failed to extract issue URL from output: {result}")
+ return {"error": "Failed to extract issue URL from command output"}
+
def create_server(host: str = "127.0.0.1", port: int = 8080) -> FastMCP:
"""Create and configure the FastMCP server.
@@ -190,6 +249,44 @@ def create_server(host: str = "127.0.0.1", port: int = 8080) -> FastMCP:
"""
return gitlab.find_project(project_name, working_directory)
+ # Add create_issue tool
+ @mcp.tool()
+ def create_issue(
+ title: str,
+ description: str,
+ working_directory: str,
+ labels: list[str] | None = None,
+ assignee: list[str] | None = None,
+ milestone: str | None = None,
+ epic_id: int | None = None,
+ project: str | None = None,
+ ) -> dict[str, Any]:
+ """Create a new GitLab issue.
+
+ Args:
+ title: The issue title.
+ description: The issue description.
+ labels: Optional list of labels to apply to the issue.
+ assignee: Optional list of usernames to assign the issue to.
+ milestone: Optional milestone title or ID.
+ epic_id: Optional ID of the epic to add the issue to.
+ project: Optional project name or path.
+ working_directory: The directory to execute the command in.
+
+ Returns:
+ A dictionary containing the issue URL or an error message.
+ """
+ return gitlab.create_issue(
+ title=title,
+ description=description,
+ working_directory=working_directory,
+ labels=labels,
+ assignee=assignee,
+ milestone=milestone,
+ epic_id=epic_id,
+ project=project,
+ )
+
return mcp
diff --git a/servers/gitlab_glab/tests/test_integration.py b/servers/gitlab_glab/tests/test_integration.py
index b25e715..6915348 100644
--- a/servers/gitlab_glab/tests/test_integration.py
+++ b/servers/gitlab_glab/tests/test_integration.py
@@ -23,7 +23,7 @@ class TestIntegration:
mock_fastmcp.assert_called_once_with("GitLab CLI", host="127.0.0.1", port=8080)
# Verify tools were registered
- assert mock_server.tool.call_count == 2
+ assert mock_server.tool.call_count == 3 # check_availability, find_project, create_issue
# 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 e612173..0c7ce64 100644
--- a/servers/gitlab_glab/tests/test_server.py
+++ b/servers/gitlab_glab/tests/test_server.py
@@ -205,7 +205,7 @@ class TestGitLabServer:
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
+ ["api", "/projects?search_namespaces=true&search=test-project"], working_dir
)
@patch.object(GitLabServer, "execute_glab_command")
@@ -262,7 +262,7 @@ class TestGitLabServer:
assert result[2]["path_with_namespace"] == "group/test-project-3"
mock_execute.assert_called_once_with(
- ["api", "/projects?search=test-project"], working_dir
+ ["api", "/projects?search_namespaces=true&search=test-project"], working_dir
)
@patch.object(GitLabServer, "execute_glab_command")
@@ -278,7 +278,7 @@ class TestGitLabServer:
assert "error" in result
assert "not found" in result["error"]
mock_execute.assert_called_once_with(
- ["api", "/projects?search=nonexistent-project"], working_dir
+ ["api", "/projects?search_namespaces=true&search=nonexistent-project"], working_dir
)
@patch.object(GitLabServer, "execute_glab_command")
@@ -294,7 +294,112 @@ class TestGitLabServer:
assert "error" in result
assert result["error"] == "API error"
mock_execute.assert_called_once_with(
- ["api", "/projects?search=test-project"], working_dir
+ ["api", "/projects?search_namespaces=true&search=test-project"], working_dir
+ )
+
+ @patch.object(GitLabServer, "execute_glab_command")
+ def test_create_issue_success(self, mock_execute: MagicMock) -> None:
+ """Test successful issue creation with required parameters."""
+ # Mock successful command execution with actual glab output format
+ mock_execute.return_value = (True, """- Creating issue in group/project
+#1 Test Issue (less than a minute ago)
+ https://gitlab.com/group/project/issues/1""")
+
+ server = GitLabServer()
+ working_dir = "/test/directory"
+ result = server.create_issue(
+ title="Test Issue",
+ description="Test Description",
+ working_directory=working_dir,
+ )
+
+ assert "url" in result
+ assert result["url"] == "https://gitlab.com/group/project/issues/1"
+ mock_execute.assert_called_once_with(
+ ["issue", "create", "-y", "-t", "Test Issue", "-d", "Test Description"],
+ working_dir,
+ )
+
+ @patch.object(GitLabServer, "execute_glab_command")
+ def test_create_issue_with_all_params(self, mock_execute: MagicMock) -> None:
+ """Test issue creation with all optional parameters."""
+ # Mock successful command execution with actual glab output format
+ mock_execute.return_value = (True, """- Creating issue in group/project
+#2 Test Issue (less than a minute ago)
+ https://gitlab.com/group/project/issues/2""")
+
+ server = GitLabServer()
+ working_dir = "/test/directory"
+ result = server.create_issue(
+ title="Test Issue",
+ description="Test Description",
+ working_directory=working_dir,
+ labels=["bug", "critical"],
+ assignee=["user1", "user2"],
+ milestone="v1.0",
+ epic_id=123,
+ project="group/project",
+ )
+
+ assert "url" in result
+ assert result["url"] == "https://gitlab.com/group/project/issues/2"
+ mock_execute.assert_called_once_with(
+ [
+ "issue", "create", "-y",
+ "-t", "Test Issue",
+ "-d", "Test Description",
+ "-l", "bug,critical",
+ "-a", "user1",
+ "-a", "user2",
+ "-m", "v1.0",
+ "--epic", "123",
+ "-R", "group/project",
+ ],
+ working_dir,
+ )
+
+ @patch.object(GitLabServer, "execute_glab_command")
+ def test_create_issue_failure(self, mock_execute: MagicMock) -> None:
+ """Test failed issue creation."""
+ # Mock failed command execution
+ mock_execute.return_value = (False, {"error": "Failed to create issue"})
+
+ server = GitLabServer()
+ working_dir = "/test/directory"
+ result = server.create_issue(
+ title="Test Issue",
+ description="Test Description",
+ working_directory=working_dir,
+ )
+
+ assert "error" in result
+ assert result["error"] == "Failed to create issue"
+ mock_execute.assert_called_once_with(
+ ["issue", "create", "-y", "-t", "Test Issue", "-d", "Test Description"],
+ working_dir,
+ )
+
+ @patch.object(GitLabServer, "execute_glab_command")
+ def test_create_issue_invalid_output(self, mock_execute: MagicMock) -> None:
+ """Test issue creation with invalid output format."""
+ # Mock successful command execution but without a URL in the output
+ mock_execute.return_value = (True, """- Creating issue in group/project
+#3 Test Issue (less than a minute ago)
+ Invalid URL format""")
+
+ server = GitLabServer()
+ working_dir = "/test/directory"
+ result = server.create_issue(
+ title="Test Issue",
+ description="Test Description",
+ working_directory=working_dir,
+ )
+
+ assert "error" in result
+ assert result["error"] == "Failed to extract issue URL from command output"
+ mock_execute.assert_called_once_with(
+ ["issue", "create", "-y", "-t", "Test Issue", "-d", "Test Description"],
+ working_dir,
)
@patch("subprocess.run")