diff options
| author | Dawid Rycerz <dawid@rycerz.xyz> | 2025-07-15 17:43:07 +0300 |
|---|---|---|
| committer | Dawid Rycerz <dawid@rycerz.xyz> | 2025-07-15 17:43:07 +0300 |
| commit | 9fbb9b43fa81ce04665fddbaaa64ab455158a44e (patch) | |
| tree | ed69c6257b590acd6942d34eccc6e75231f92b02 /servers | |
| parent | ea094d5731ae36db599b9a9803c41db303fc685a (diff) | |
feat: Add listing and adding comments
Diffstat (limited to 'servers')
| -rw-r--r-- | servers/gitlab_python/src/mcp_server_gitlab_python/server.py | 200 | ||||
| -rw-r--r-- | servers/gitlab_python/tests/test_server.py | 142 |
2 files changed, 342 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 c176832..6688a1a 100644 --- a/servers/gitlab_python/src/mcp_server_gitlab_python/server.py +++ b/servers/gitlab_python/src/mcp_server_gitlab_python/server.py @@ -389,6 +389,126 @@ class GitLabPythonServer: logger.error(f"Failed to update epic {group}#{epic_iid}: {e}") return {"error": str(e)} + def list_issue_comments( + self, project: str, issue_iid: int + ) -> dict[str, Any]: + """List all comments (notes) for a given issue. + + Args: + project (str): The project full path or ID. + issue_iid (int): The internal ID of the issue. + + Returns: + dict[str, Any]: Dictionary with a list of comments or error message. + """ + try: + proj = self.gl.projects.get(project) + issue = proj.issues.get(issue_iid) + notes = issue.notes.list(all=True) + return { + "comments": [ + { + "id": n.id, + "body": n.body, + "author": getattr(n, "author", None), + "created_at": n.created_at, + "updated_at": n.updated_at, + "system": getattr(n, "system", False), + } + for n in notes + ] + } + except Exception as e: + return {"error": str(e)} + + def create_issue_comment( + self, project: str, issue_iid: int, body: str + ) -> dict[str, Any]: + """Create a comment (note) on a given issue. + + Args: + project (str): The project full path or ID. + issue_iid (int): The internal ID of the issue. + body (str): The comment text. + + Returns: + dict[str, Any]: Dictionary with the comment info or error message. + """ + try: + proj = self.gl.projects.get(project) + issue = proj.issues.get(issue_iid) + note = issue.notes.create({"body": body}) + return { + "id": note.id, + "body": note.body, + "author": getattr(note, "author", None), + "created_at": note.created_at, + "updated_at": note.updated_at, + "system": getattr(note, "system", False), + } + except Exception as e: + return {"error": str(e)} + + def list_epic_comments( + self, group: str, epic_iid: int + ) -> dict[str, Any]: + """List all comments (notes) for a given epic. + + Args: + group (str): The group full path or ID. + epic_iid (int): The internal ID of the epic. + + Returns: + dict[str, Any]: Dictionary with a list of comments or error message. + """ + try: + grp = self.gl.groups.get(group) + epic = grp.epics.get(epic_iid) + notes = epic.notes.list(all=True) + return { + "comments": [ + { + "id": n.id, + "body": n.body, + "author": getattr(n, "author", None), + "created_at": n.created_at, + "updated_at": n.updated_at, + "system": getattr(n, "system", False), + } + for n in notes + ] + } + except Exception as e: + return {"error": str(e)} + + def create_epic_comment( + self, group: str, epic_iid: int, body: str + ) -> dict[str, Any]: + """Create a comment (note) on a given epic. + + Args: + group (str): The group full path or ID. + epic_iid (int): The internal ID of the epic. + body (str): The comment text. + + Returns: + dict[str, Any]: Dictionary with the comment info or error message. + """ + try: + grp = self.gl.groups.get(group) + epic = grp.epics.get(epic_iid) + note = epic.notes.create({"body": body}) + return { + "id": note.id, + "body": note.body, + "author": getattr(note, "author", None), + "created_at": note.created_at, + "updated_at": note.updated_at, + "system": getattr(note, "system", False), + } + except Exception as 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) @@ -616,6 +736,86 @@ def create_server(host: str = "127.0.0.1", port: int = 8080) -> FastMCP: data.update(kwargs) return server.update_epic(group, epic_iid, **data) + @mcp.tool() + def list_issue_comments( + project: str, + issue_iid: int, + working_directory: str, + ) -> dict[str, Any]: + """List all comments (notes) for a given issue. + + Args: + project (str): The project full path or ID. + issue_iid (int): The internal ID of the issue. + working_directory (str): The working directory for context. + + Returns: + dict[str, Any]: Dictionary with a list of comments or error message. + """ + server = GitLabPythonServer(working_directory) + return server.list_issue_comments(project, issue_iid) + + @mcp.tool() + def create_issue_comment( + project: str, + issue_iid: int, + body: str, + working_directory: str, + ) -> dict[str, Any]: + """Create a comment (note) on a given issue. + + Args: + project (str): The project full path or ID. + issue_iid (int): The internal ID of the issue. + body (str): The comment text. + working_directory (str): The working directory for context. + + Returns: + dict[str, Any]: Dictionary with the comment info or error message. + """ + server = GitLabPythonServer(working_directory) + return server.create_issue_comment(project, issue_iid, body) + + @mcp.tool() + def list_epic_comments( + group: str, + epic_iid: int, + working_directory: str, + ) -> dict[str, Any]: + """List all comments (notes) for a given epic. + + Args: + group (str): The group full path or ID. + epic_iid (int): The internal ID of the epic. + working_directory (str): The working directory for context. + + Returns: + dict[str, Any]: Dictionary with a list of comments or error message. + """ + server = GitLabPythonServer(working_directory) + return server.list_epic_comments(group, epic_iid) + + @mcp.tool() + def create_epic_comment( + group: str, + epic_iid: int, + body: str, + working_directory: str, + ) -> dict[str, Any]: + """Create a comment (note) on a given epic. + + Args: + group (str): The group full path or ID. + epic_iid (int): The internal ID of the epic. + body (str): The comment text. + working_directory (str): The working directory for context. + + Returns: + dict[str, Any]: Dictionary with the comment info or error message. + """ + server = GitLabPythonServer(working_directory) + return server.create_epic_comment(group, epic_iid, body) + 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 244cdd5..94820aa 100644 --- a/servers/gitlab_python/tests/test_server.py +++ b/servers/gitlab_python/tests/test_server.py @@ -335,4 +335,146 @@ def test_update_epic_error(mock_gitlab, mock_settings): 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"] + +@patch("mcp_server_gitlab_python.server.get_gitlab_settings", return_value=("https://gitlab.com", "dummy-token")) +@patch("gitlab.Gitlab") +def test_list_issue_comments_success(mock_gitlab, mock_settings): + server = GitLabPythonServer(working_directory="/tmp") + proj = MagicMock() + issue = MagicMock() + note1 = MagicMock() + note1.id = 1 + note1.body = "First comment" + note1.author = {"id": 2, "name": "Alice"} + note1.created_at = "2024-01-01T00:00:00Z" + note1.updated_at = "2024-01-01T01:00:00Z" + note1.system = False + note2 = MagicMock() + note2.id = 2 + note2.body = "Second comment" + note2.author = {"id": 3, "name": "Bob"} + note2.created_at = "2024-01-02T00:00:00Z" + note2.updated_at = "2024-01-02T01:00:00Z" + note2.system = True + issue.notes.list.return_value = [note1, note2] + proj.issues.get.return_value = issue + mock_gitlab.return_value.projects.get.return_value = proj + result = server.list_issue_comments("project/path", 42) + assert "comments" in result + assert len(result["comments"]) == 2 + assert result["comments"][0]["body"] == "First comment" + assert result["comments"][1]["author"]["name"] == "Bob" + proj.issues.get.assert_called_once_with(42) + issue.notes.list.assert_called_once_with(all=True) + +@patch("mcp_server_gitlab_python.server.get_gitlab_settings", return_value=("https://gitlab.com", "dummy-token")) +@patch("gitlab.Gitlab") +def test_list_issue_comments_error(mock_gitlab, mock_settings): + server = GitLabPythonServer(working_directory="/tmp") + mock_gitlab.return_value.projects.get.side_effect = Exception("Project not found") + result = server.list_issue_comments("bad-project", 42) + 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_create_issue_comment_success(mock_gitlab, mock_settings): + server = GitLabPythonServer(working_directory="/tmp") + proj = MagicMock() + issue = MagicMock() + note = MagicMock() + note.id = 123 + note.body = "A new comment" + note.author = {"id": 2, "name": "Alice"} + note.created_at = "2024-01-01T00:00:00Z" + note.updated_at = "2024-01-01T01:00:00Z" + note.system = False + issue.notes.create.return_value = note + proj.issues.get.return_value = issue + mock_gitlab.return_value.projects.get.return_value = proj + result = server.create_issue_comment("project/path", 42, "A new comment") + assert result["id"] == 123 + assert result["body"] == "A new comment" + assert result["author"]["name"] == "Alice" + issue.notes.create.assert_called_once_with({"body": "A new comment"}) + +@patch("mcp_server_gitlab_python.server.get_gitlab_settings", return_value=("https://gitlab.com", "dummy-token")) +@patch("gitlab.Gitlab") +def test_create_issue_comment_error(mock_gitlab, mock_settings): + server = GitLabPythonServer(working_directory="/tmp") + mock_gitlab.return_value.projects.get.side_effect = Exception("Project not found") + result = server.create_issue_comment("bad-project", 42, "A new comment") + 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_list_epic_comments_success(mock_gitlab, mock_settings): + server = GitLabPythonServer(working_directory="/tmp") + group = MagicMock() + epic = MagicMock() + note1 = MagicMock() + note1.id = 1 + note1.body = "Epic comment 1" + note1.author = {"id": 2, "name": "Alice"} + note1.created_at = "2024-01-01T00:00:00Z" + note1.updated_at = "2024-01-01T01:00:00Z" + note1.system = False + note2 = MagicMock() + note2.id = 2 + note2.body = "Epic comment 2" + note2.author = {"id": 3, "name": "Bob"} + note2.created_at = "2024-01-02T00:00:00Z" + note2.updated_at = "2024-01-02T01:00:00Z" + note2.system = True + epic.notes.list.return_value = [note1, note2] + group.epics.get.return_value = epic + mock_gitlab.return_value.groups.get.return_value = group + result = server.list_epic_comments("test-group", 101) + assert "comments" in result + assert len(result["comments"]) == 2 + assert result["comments"][0]["body"] == "Epic comment 1" + assert result["comments"][1]["author"]["name"] == "Bob" + group.epics.get.assert_called_once_with(101) + epic.notes.list.assert_called_once_with(all=True) + +@patch("mcp_server_gitlab_python.server.get_gitlab_settings", return_value=("https://gitlab.com", "dummy-token")) +@patch("gitlab.Gitlab") +def test_list_epic_comments_error(mock_gitlab, mock_settings): + server = GitLabPythonServer(working_directory="/tmp") + mock_gitlab.return_value.groups.get.side_effect = Exception("Group not found") + result = server.list_epic_comments("bad-group", 101) + 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_create_epic_comment_success(mock_gitlab, mock_settings): + server = GitLabPythonServer(working_directory="/tmp") + group = MagicMock() + epic = MagicMock() + note = MagicMock() + note.id = 456 + note.body = "A new epic comment" + note.author = {"id": 2, "name": "Alice"} + note.created_at = "2024-01-01T00:00:00Z" + note.updated_at = "2024-01-01T01:00:00Z" + note.system = False + epic.notes.create.return_value = note + group.epics.get.return_value = epic + mock_gitlab.return_value.groups.get.return_value = group + result = server.create_epic_comment("test-group", 101, "A new epic comment") + assert result["id"] == 456 + assert result["body"] == "A new epic comment" + assert result["author"]["name"] == "Alice" + epic.notes.create.assert_called_once_with({"body": "A new epic comment"}) + +@patch("mcp_server_gitlab_python.server.get_gitlab_settings", return_value=("https://gitlab.com", "dummy-token")) +@patch("gitlab.Gitlab") +def test_create_epic_comment_error(mock_gitlab, mock_settings): + server = GitLabPythonServer(working_directory="/tmp") + mock_gitlab.return_value.groups.get.side_effect = Exception("Group not found") + result = server.create_epic_comment("bad-group", 101, "A new epic comment") + assert "error" in result assert "Group not found" in result["error"]
\ No newline at end of file |
