diff options
| author | Dawid Rycerz <dawid@rycerz.xyz> | 2025-07-15 17:36:46 +0300 |
|---|---|---|
| committer | Dawid Rycerz <dawid@rycerz.xyz> | 2025-07-15 17:36:46 +0300 |
| commit | ea094d5731ae36db599b9a9803c41db303fc685a (patch) | |
| tree | 873145a3bc4cc3f5140a5ca12d9d84bf180a1b54 /servers/gitlab_python | |
| parent | 0d3b300943d44ad69f803f2e196bc643b937ef37 (diff) | |
feat: Add updates issues and epics functinoality
Diffstat (limited to 'servers/gitlab_python')
| -rw-r--r-- | servers/gitlab_python/src/mcp_server_gitlab_python/server.py | 127 | ||||
| -rw-r--r-- | servers/gitlab_python/tests/test_server.py | 58 |
2 files changed, 185 insertions, 0 deletions
diff --git a/servers/gitlab_python/src/mcp_server_gitlab_python/server.py b/servers/gitlab_python/src/mcp_server_gitlab_python/server.py index 05e84c5..c176832 100644 --- a/servers/gitlab_python/src/mcp_server_gitlab_python/server.py +++ b/servers/gitlab_python/src/mcp_server_gitlab_python/server.py @@ -152,6 +152,33 @@ class GitLabPythonServer: except Exception as e: return {"error": str(e)} + def update_issue( + self, + project: str, + issue_iid: int, + **kwargs: object, + ) -> dict[str, Any]: + """Update an existing GitLab issue. + + Args: + project (str): The project full path or ID. + issue_iid (int): The internal ID of the issue. + **kwargs: Fields to update (e.g., title, description, labels, etc.). + + Returns: + dict[str, Any]: Dictionary with the issue URL or error message. + """ + try: + proj = self.gl.projects.get(project) + issue = proj.issues.get(issue_iid) + for k, v in kwargs.items(): + setattr(issue, k, v) + issue.save() + return {"url": issue.web_url} + except Exception as e: + logger.error(f"Failed to update issue {project}#{issue_iid}: {e}") + return {"error": str(e)} + def get_mr_diff( self, project: str, @@ -335,6 +362,33 @@ class GitLabPythonServer: except Exception as e: return {"error": str(e)} + def update_epic( + self, + group: str, + epic_iid: int, + **kwargs: object, + ) -> dict[str, Any]: + """Update an existing GitLab epic. + + Args: + group (str): The group full path or ID. + epic_iid (int): The internal ID of the epic. + **kwargs: Fields to update (e.g., title, description, labels, etc.). + + Returns: + dict[str, Any]: Dictionary with the epic URL or error message. + """ + try: + grp = self.gl.groups.get(group) + epic = grp.epics.get(epic_iid) + for k, v in kwargs.items(): + setattr(epic, k, v) + epic.save() + return {"url": epic.web_url} + except Exception as e: + logger.error(f"Failed to update epic {group}#{epic_iid}: {e}") + return {"error": str(e)} + def create_server(host: str = "127.0.0.1", port: int = 8080) -> FastMCP: mcp = FastMCP("GitLab Python", host=host, port=port) @@ -391,6 +445,37 @@ def create_server(host: str = "127.0.0.1", port: int = 8080) -> FastMCP: return server.create_issue(project, title, description, **data) @mcp.tool() + def update_issue( + project: str, + issue_iid: int, + working_directory: str, + title: str | None = None, + description: str | None = None, + state_event: str | None = None, + labels: list[str] | None = None, + assignee_ids: list[int] | None = None, + milestone_id: int | None = None, + **kwargs: object + ) -> dict[str, Any]: + """Update an existing GitLab issue.""" + server = GitLabPythonServer(working_directory) + data = {} + if title is not None: + data["title"] = title + if description is not None: + data["description"] = description + if state_event is not None: + data["state_event"] = state_event + if labels is not None: + data["labels"] = labels + if assignee_ids is not None: + data["assignee_ids"] = assignee_ids + if milestone_id is not None: + data["milestone_id"] = milestone_id + data.update(kwargs) + return server.update_issue(project, issue_iid, **data) + + @mcp.tool() def get_mr_diff( project: str, mr_iid: int, @@ -489,6 +574,48 @@ def create_server(host: str = "127.0.0.1", port: int = 8080) -> FastMCP: data.update(kwargs) return server.create_epic(group, title, description, **data) + @mcp.tool() + def update_epic( + group: str, + epic_iid: int, + working_directory: str, + title: str | None = None, + description: str | None = None, + state_event: str | None = None, + labels: list[str] | None = None, + parent_id: int | None = None, + color: str | None = None, + confidential: bool | None = None, + start_date_fixed: str | None = None, + due_date_fixed: str | None = None, + **kwargs: object + ) -> dict[str, Any]: + """Update an existing GitLab epic.""" + server = GitLabPythonServer(working_directory) + data = {} + if title is not None: + data["title"] = title + if description is not None: + data["description"] = description + if state_event is not None: + data["state_event"] = state_event + if labels is not None: + data["labels"] = labels + if parent_id is not None: + data["parent_id"] = parent_id + if color is not None: + data["color"] = color + if confidential is not None: + data["confidential"] = confidential + if start_date_fixed is not None: + data["start_date_fixed"] = start_date_fixed + data["start_date_is_fixed"] = True + if due_date_fixed is not None: + data["due_date_fixed"] = due_date_fixed + data["due_date_is_fixed"] = True + data.update(kwargs) + return server.update_epic(group, epic_iid, **data) + return mcp async def main(transport_type: str, host: str, port: int) -> None: diff --git a/servers/gitlab_python/tests/test_server.py b/servers/gitlab_python/tests/test_server.py index 7c0e69f..244cdd5 100644 --- a/servers/gitlab_python/tests/test_server.py +++ b/servers/gitlab_python/tests/test_server.py @@ -277,4 +277,62 @@ def test_create_epic_error(mock_gitlab, mock_settings): mock_gitlab.return_value.groups.get.side_effect = Exception("Group not found") result = server.create_epic("bad-group", "Epic Title", "Epic description") assert "error" in result + assert "Group not found" in result["error"] + +@patch("mcp_server_gitlab_python.server.get_gitlab_settings", return_value=("https://gitlab.com", "dummy-token")) +@patch("gitlab.Gitlab") +def test_update_issue_success(mock_gitlab, mock_settings): + """Test successful update of a GitLab issue.""" + server = GitLabPythonServer(working_directory="/tmp") + proj = MagicMock() + issue = MagicMock() + issue.web_url = "https://gitlab.com/project/-/issues/42" + proj.issues.get.return_value = issue + mock_gitlab.return_value.projects.get.return_value = proj + result = server.update_issue("project/path", 42, title="Updated Title", description="Updated desc", labels=["bug"]) + assert "url" in result + assert result["url"] == "https://gitlab.com/project/-/issues/42" + proj.issues.get.assert_called_once_with(42) + assert issue.title == "Updated Title" + assert issue.description == "Updated desc" + assert issue.labels == ["bug"] + issue.save.assert_called_once() + +@patch("mcp_server_gitlab_python.server.get_gitlab_settings", return_value=("https://gitlab.com", "dummy-token")) +@patch("gitlab.Gitlab") +def test_update_issue_error(mock_gitlab, mock_settings): + """Test error handling for update_issue when project or issue is not found.""" + server = GitLabPythonServer(working_directory="/tmp") + mock_gitlab.return_value.projects.get.side_effect = Exception("Project not found") + result = server.update_issue("bad-project", 42, title="Updated Title") + assert "error" in result + assert "Project not found" in result["error"] + +@patch("mcp_server_gitlab_python.server.get_gitlab_settings", return_value=("https://gitlab.com", "dummy-token")) +@patch("gitlab.Gitlab") +def test_update_epic_success(mock_gitlab, mock_settings): + """Test successful update of a GitLab epic.""" + server = GitLabPythonServer(working_directory="/tmp") + group = MagicMock() + epic = MagicMock() + epic.web_url = "https://gitlab.com/groups/test-group/-/epics/101" + group.epics.get.return_value = epic + mock_gitlab.return_value.groups.get.return_value = group + result = server.update_epic("test-group", 101, title="Updated Epic", description="Updated desc", labels=["feature"]) + assert "url" in result + assert result["url"] == "https://gitlab.com/groups/test-group/-/epics/101" + group.epics.get.assert_called_once_with(101) + assert epic.title == "Updated Epic" + assert epic.description == "Updated desc" + assert epic.labels == ["feature"] + epic.save.assert_called_once() + +@patch("mcp_server_gitlab_python.server.get_gitlab_settings", return_value=("https://gitlab.com", "dummy-token")) +@patch("gitlab.Gitlab") +def test_update_epic_error(mock_gitlab, mock_settings): + """Test error handling for update_epic when group or epic is not found.""" + server = GitLabPythonServer(working_directory="/tmp") + mock_gitlab.return_value.groups.get.side_effect = Exception("Group not found") + result = server.update_epic("bad-group", 101, title="Updated Epic") + assert "error" in result assert "Group not found" in result["error"]
\ No newline at end of file |
