summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDawid Rycerz <dawid@rycerz.xyz>2025-07-15 16:54:54 +0300
committerDawid Rycerz <dawid@rycerz.xyz>2025-07-15 16:54:54 +0300
commit4e89e51901d9b3726a56d15980a0845b8e2a36b0 (patch)
treed1639d767da3a9b82254295560d24f68dbba88df
parentbacbd1b1382c3a7547ff0a63ad75fbdd4522db84 (diff)
feat: add epics functionality
-rw-r--r--.cursorrules3
-rw-r--r--pyproject.toml1
-rw-r--r--servers/gitlab_python/pyproject.toml5
-rw-r--r--servers/gitlab_python/src/mcp_server_gitlab_python/cli.py2
-rw-r--r--servers/gitlab_python/src/mcp_server_gitlab_python/server.py245
-rw-r--r--servers/gitlab_python/tests/test_server.py107
-rw-r--r--servers/gitlab_python/uv.lock37
7 files changed, 328 insertions, 72 deletions
diff --git a/.cursorrules b/.cursorrules
index afd9955..a4d098d 100644
--- a/.cursorrules
+++ b/.cursorrules
@@ -80,4 +80,5 @@
- **When providing solutions or examples, ensure they are self-contained and executable without requiring extensive modifications.**
- **If a request is unclear or lacks sufficient information, ask clarifying questions before proceeding.**
- **Always consider the security implications of your code, especially when dealing with user inputs and external data.**
-- **Actively use and promote best practices for the specific tasks at hand (LLM app development, data cleaning, demo creation, etc.).** \ No newline at end of file
+- **Actively use and promote best practices for the specific tasks at hand (LLM app development, data cleaning, demo creation, etc.).**
+- **Run all tests by changing directory to subproject and run `uv run pytest` for dependency context** \ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 90c29bf..1e8b0d6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -32,6 +32,7 @@ dev = [
target-version = "py310"
line-length = 88
select = ["E", "F", "B", "I", "N", "UP", "ANN", "S", "A"]
+exclude = ["**/test_*.py"]
[tool.ruff.isort]
known-first-party = ["shared"]
diff --git a/servers/gitlab_python/pyproject.toml b/servers/gitlab_python/pyproject.toml
index 30ad43d..1b7bd1d 100644
--- a/servers/gitlab_python/pyproject.toml
+++ b/servers/gitlab_python/pyproject.toml
@@ -11,8 +11,9 @@ authors = [
dependencies = [
"mcp>=1.6.0",
"pydantic>=2.11.0",
- "python-gitlab>=4.4.0",
- "PyYAML>=6.0.0"
+ "python-gitlab>=6.1.0",
+ "PyYAML>=6.0.0",
+ "GitPython>=3.1.43",
]
[tool.uv]
diff --git a/servers/gitlab_python/src/mcp_server_gitlab_python/cli.py b/servers/gitlab_python/src/mcp_server_gitlab_python/cli.py
index 1b4db35..323c46a 100644
--- a/servers/gitlab_python/src/mcp_server_gitlab_python/cli.py
+++ b/servers/gitlab_python/src/mcp_server_gitlab_python/cli.py
@@ -6,8 +6,8 @@ Common CLI functionality for the GitLab Python MCP server.
import argparse
import asyncio
import logging
-import sys
import os
+import sys
from .server import main
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 1b6ba76..05e84c5 100644
--- a/servers/gitlab_python/src/mcp_server_gitlab_python/server.py
+++ b/servers/gitlab_python/src/mcp_server_gitlab_python/server.py
@@ -1,26 +1,23 @@
import logging
import os
-import sys
-import yaml
+from typing import Any
+
import gitlab
-import subprocess
-from typing import Any, Optional
+import yaml
from mcp.server.fastmcp import FastMCP
logger = logging.getLogger("mcp_gitlab_python_server")
-def get_git_remote_url(working_directory: str) -> Optional[str]:
+def get_git_remote_url(working_directory: str) -> str | None:
try:
- result = subprocess.run([
- "git", "remote", "get-url", "origin"
- ], capture_output=True, text=True, check=False, cwd=working_directory)
- if result.returncode == 0:
- return result.stdout.strip()
+ from git import Repo
+ repo = Repo(working_directory)
+ return repo.remotes.origin.url
except Exception as e:
logger.warning(f"Could not get git remote url: {e}")
return None
-def parse_gitlab_url_from_remote(remote_url: str) -> Optional[str]:
+def parse_gitlab_url_from_remote(remote_url: str) -> str | None:
# Handles both SSH and HTTPS remotes
if remote_url.startswith("git@"):
# git@gitlab.com:namespace/project.git
@@ -33,28 +30,22 @@ def parse_gitlab_url_from_remote(remote_url: str) -> Optional[str]:
return f"{parts[0]}//{parts[2]}"
return None
-def get_token_from_glab_config(host: str) -> Optional[str]:
+def get_token_from_glab_config(host: str) -> str | None:
"""
Retrieve the GitLab token for a specific host from the glab-cli config file.
-
- Args:
- host (str): The GitLab host (e.g., 'gitlab.com').
-
- Returns:
- Optional[str]: The token if found, otherwise None.
"""
config_path = os.path.expanduser("~/.config/glab-cli/config.yml")
if not os.path.exists(config_path):
return None
try:
- with open(config_path, "r") as f:
+ with open(config_path) as f:
config = yaml.safe_load(f)
hosts = config.get('hosts', {})
# Try direct match
if host in hosts and 'token' in hosts[host]:
return hosts[host]['token']
# Try matching by api_host if present
- for h, data in hosts.items():
+ for _h, data in hosts.items(): # Renamed h to _h for Ruff B007
if data.get('api_host') == host and 'token' in data:
return data['token']
except Exception as e:
@@ -86,18 +77,25 @@ def get_gitlab_settings(working_directory: str) -> tuple[str, str]:
# Extract host from URL
from urllib.parse import urlparse
parsed = urlparse(url)
- host = parsed.hostname or url.replace("https://", "").replace("http://", "").split("/")[0]
+ host = (
+ parsed.hostname
+ or url.replace("https://", "").replace("http://", "").split("/")[0]
+ )
# Token
token = os.environ.get("GITLAB_TOKEN")
if not token:
token = get_token_from_glab_config(host)
if not token:
- logger.error(f"No GitLab token found for host '{host}' in env or glab-cli config.")
- raise RuntimeError(f"No GitLab token found for host '{host}' in env or glab-cli config.")
+ logger.error(
+ f"No GitLab token found for host '{host}' in env or glab-cli config."
+ )
+ raise RuntimeError(
+ f"No GitLab token found for host '{host}' in env or glab-cli config."
+ )
return url, token
class GitLabPythonServer:
- def __init__(self, working_directory: str):
+ def __init__(self, working_directory: str) -> None:
url, token = get_gitlab_settings(working_directory)
self.gl = gitlab.Gitlab(url, private_token=token)
self.gl.auth()
@@ -115,7 +113,7 @@ class GitLabPythonServer:
for p in projects
]
- def search_issues(self, project: str, **filters) -> dict[str, Any]:
+ def search_issues(self, project: str, **filters: object) -> dict[str, Any]:
try:
proj = self.gl.projects.get(project)
issues = proj.issues.list(**filters, all=True)
@@ -136,10 +134,20 @@ class GitLabPythonServer:
except Exception as e:
return {"error": str(e)}
- def create_issue(self, project: str, title: str, description: str, **kwargs) -> dict[str, Any]:
+ def create_issue(
+ self,
+ project: str,
+ title: str,
+ description: str,
+ **kwargs: object,
+ ) -> dict[str, Any]:
try:
proj = self.gl.projects.get(project)
- issue = proj.issues.create({"title": title, "description": description, **kwargs})
+ issue = proj.issues.create({
+ "title": title,
+ "description": description,
+ **kwargs,
+ })
return {"url": issue.web_url}
except Exception as e:
return {"error": str(e)}
@@ -149,7 +157,7 @@ class GitLabPythonServer:
project: str,
mr_iid: int,
max_size_kb: int = 100,
- filter_extensions: Optional[list[str]] = None,
+ filter_extensions: list[str] | None = None,
) -> dict[str, Any]:
import tempfile
if filter_extensions is None:
@@ -214,22 +222,19 @@ class GitLabPythonServer:
self,
project: str,
branch: str = None,
- variables: Optional[dict] = None,
+ variables: dict | None = None,
web_mode: bool = False,
working_directory: str = None,
) -> dict[str, Any]:
- import subprocess
try:
proj = self.gl.projects.get(project)
ref = branch
if not ref:
- # Try to detect current branch
+ # Try to detect current branch using GitPython
try:
- result = subprocess.run([
- "git", "branch", "--show-current"
- ], capture_output=True, text=True, check=False, cwd=working_directory)
- if result.returncode == 0 and result.stdout.strip():
- ref = result.stdout.strip()
+ from git import Repo
+ repo = Repo(working_directory)
+ ref = repo.active_branch.name
except Exception:
ref = None
if not ref:
@@ -256,6 +261,80 @@ class GitLabPythonServer:
except Exception as e:
return {"error": str(e)}
+ def find_group(self, group_name: str) -> list[dict[str, Any]]:
+ """Find GitLab groups by name or path."""
+ groups = self.gl.groups.list(search=group_name, all=True)
+ return [
+ {
+ "id": g.id,
+ "name": g.name,
+ "full_path": g.full_path,
+ "web_url": g.web_url,
+ "description": g.description,
+ }
+ for g in groups
+ ]
+
+ def search_epics(self, group: str, **filters: object) -> dict[str, Any]:
+ """Search for GitLab epics in a group with various filters.
+
+ Args:
+ group (str): The group full path or ID.
+ **filters: Additional filters for the search.
+
+ Returns:
+ dict[str, Any]: Dictionary with a list of epics or error message.
+ """
+ try:
+ grp = self.gl.groups.get(group)
+ epics = grp.epics.list(**filters, all=True)
+ return {
+ "epics": [
+ {
+ "id": e.id,
+ "iid": e.iid,
+ "title": e.title,
+ "web_url": e.web_url,
+ "state": e.state,
+ "created_at": e.created_at,
+ "updated_at": e.updated_at,
+ "author": getattr(e, "author", None),
+ "labels": getattr(e, "labels", []),
+ "description": getattr(e, "description", None),
+ }
+ for e in epics
+ ]
+ }
+ except Exception as e:
+ return {"error": str(e)}
+
+ def create_epic(
+ self,
+ group: str,
+ title: str,
+ description: str = "",
+ **kwargs: object,
+ ) -> dict[str, Any]:
+ """Create a new GitLab epic in a group.
+
+ Args:
+ group (str): The group full path or ID.
+ title (str): The title of the epic.
+ description (str): The description of the epic.
+ **kwargs: Additional fields for the epic.
+
+ Returns:
+ dict[str, Any]: Dictionary with the epic URL or error message.
+ """
+ try:
+ grp = self.gl.groups.get(group)
+ data = {"title": title, "description": description}
+ data.update(kwargs)
+ epic = grp.epics.create(data)
+ return {"url": epic.web_url}
+ 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)
@@ -269,16 +348,20 @@ def create_server(host: str = "127.0.0.1", port: int = 8080) -> FastMCP:
def search_issues(
project: str,
working_directory: str,
- author_id: Optional[int] = None,
- assignee_id: Optional[int] = None,
- state: Optional[str] = None,
- labels: Optional[list[str]] = None,
- milestone: Optional[str] = None,
- **kwargs
+ author_id: int | None = None,
+ assignee_id: int | None = None,
+ state: str | None = None,
+ labels: list[str] | None = None,
+ milestone: str | None = None,
+ **kwargs: object
) -> dict[str, Any]:
"""Search for GitLab issues with various filters."""
server = GitLabPythonServer(working_directory)
- filters = {k: v for k, v in locals().items() if v is not None and k not in ["project", "working_directory", "kwargs"]}
+ filters = {
+ k: v
+ for k, v in locals().items()
+ if v is not None and k not in ["project", "working_directory", "kwargs"]
+ }
if labels:
filters["labels"] = ",".join(labels)
filters.update(kwargs)
@@ -290,10 +373,10 @@ def create_server(host: str = "127.0.0.1", port: int = 8080) -> FastMCP:
title: str,
description: str,
working_directory: str,
- labels: Optional[list[str]] = None,
- assignee_ids: Optional[list[int]] = None,
- milestone_id: Optional[int] = None,
- **kwargs
+ labels: list[str] | None = None,
+ assignee_ids: list[int] | None = None,
+ milestone_id: int | None = None,
+ **kwargs: object
) -> dict[str, Any]:
"""Create a new GitLab issue."""
server = GitLabPythonServer(working_directory)
@@ -313,7 +396,7 @@ def create_server(host: str = "127.0.0.1", port: int = 8080) -> FastMCP:
mr_iid: int,
working_directory: str,
max_size_kb: int = 100,
- filter_extensions: Optional[list[str]] = None,
+ filter_extensions: list[str] | None = None,
) -> dict[str, Any]:
"""Get the diff for a merge request."""
server = GitLabPythonServer(working_directory)
@@ -329,7 +412,7 @@ def create_server(host: str = "127.0.0.1", port: int = 8080) -> FastMCP:
project: str,
working_directory: str,
branch: str = None,
- variables: Optional[dict] = None,
+ variables: dict | None = None,
web_mode: bool = False,
) -> dict[str, Any]:
"""Run a CI/CD pipeline on GitLab."""
@@ -342,6 +425,70 @@ def create_server(host: str = "127.0.0.1", port: int = 8080) -> FastMCP:
working_directory=working_directory,
)
+ @mcp.tool()
+ def find_group(group_name: str, working_directory: str) -> list[dict[str, Any]]:
+ """Find GitLab groups by name or path."""
+ server = GitLabPythonServer(working_directory)
+ return server.find_group(group_name)
+
+ @mcp.tool()
+ def search_epics(
+ group: str,
+ working_directory: str,
+ author_id: int | None = None,
+ state: str | None = None,
+ labels: list[str] | None = None,
+ order_by: str | None = None,
+ sort: str | None = None,
+ search: str | None = None,
+ **kwargs: object
+ ) -> dict[str, Any]:
+ """Search for GitLab epics in a group with various filters."""
+ server = GitLabPythonServer(working_directory)
+ filters = {
+ k: v
+ for k, v in locals().items()
+ if v is not None and k not in ["group", "working_directory", "kwargs"]
+ }
+ if labels:
+ filters["labels"] = ",".join(labels)
+ filters.update(kwargs)
+ return server.search_epics(group, **filters)
+
+ @mcp.tool()
+ def create_epic(
+ group: str,
+ title: str,
+ description: str,
+ working_directory: str,
+ 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]:
+ """Create a new GitLab epic in a group."""
+ server = GitLabPythonServer(working_directory)
+ data = {}
+ if labels:
+ 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.create_epic(group, title, description, **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 99cb788..7c0e69f 100644
--- a/servers/gitlab_python/tests/test_server.py
+++ b/servers/gitlab_python/tests/test_server.py
@@ -111,8 +111,8 @@ def test_run_ci_pipeline_success_with_branch(mock_gitlab, mock_settings):
@patch("mcp_server_gitlab_python.server.get_gitlab_settings", return_value=("https://gitlab.com", "dummy-token"))
@patch("gitlab.Gitlab")
-@patch("subprocess.run")
-def test_run_ci_pipeline_success_current_branch(mock_subprocess, mock_gitlab, mock_settings):
+@patch("git.Repo")
+def test_run_ci_pipeline_success_current_branch(mock_repo, mock_gitlab, mock_settings):
server = GitLabPythonServer(working_directory="/tmp")
proj = MagicMock()
pipeline = MagicMock()
@@ -121,11 +121,10 @@ def test_run_ci_pipeline_success_current_branch(mock_subprocess, mock_gitlab, mo
proj.pipelines.create.return_value = pipeline
proj.default_branch = "main"
mock_gitlab.return_value.projects.get.return_value = proj
- # Simulate git branch detection
- mock_git = MagicMock()
- mock_git.returncode = 0
- mock_git.stdout = "feature-branch"
- mock_subprocess.return_value = mock_git
+ # Simulate git branch detection using GitPython
+ mock_branch = MagicMock()
+ mock_branch.name = "feature-branch"
+ mock_repo.return_value.active_branch = mock_branch
result = server.run_ci_pipeline(
project="project/path",
branch=None,
@@ -137,15 +136,12 @@ def test_run_ci_pipeline_success_current_branch(mock_subprocess, mock_gitlab, mo
assert result["pipeline_id"] == 456
assert result["pipeline_url"] == "https://gitlab.com/project/-/pipelines/456"
assert result["branch"] == "feature-branch"
- mock_subprocess.assert_called_once_with([
- "git", "branch", "--show-current"
- ], capture_output=True, text=True, check=False, cwd="/tmp")
proj.pipelines.create.assert_called_once()
@patch("mcp_server_gitlab_python.server.get_gitlab_settings", return_value=("https://gitlab.com", "dummy-token"))
@patch("gitlab.Gitlab")
-@patch("subprocess.run")
-def test_run_ci_pipeline_fallback_to_default_branch(mock_subprocess, mock_gitlab, mock_settings):
+@patch("git.Repo")
+def test_run_ci_pipeline_fallback_to_default_branch(mock_repo, mock_gitlab, mock_settings):
server = GitLabPythonServer(working_directory="/tmp")
proj = MagicMock()
pipeline = MagicMock()
@@ -154,11 +150,8 @@ def test_run_ci_pipeline_fallback_to_default_branch(mock_subprocess, mock_gitlab
proj.pipelines.create.return_value = pipeline
proj.default_branch = "main"
mock_gitlab.return_value.projects.get.return_value = proj
- # Simulate git branch detection failure
- mock_git = MagicMock()
- mock_git.returncode = 1
- mock_git.stdout = ""
- mock_subprocess.return_value = mock_git
+ # Simulate git branch detection failure by raising an exception
+ mock_repo.return_value.active_branch = property(lambda self: (_ for _ in ()).throw(Exception("No branch")))
result = server.run_ci_pipeline(
project="project/path",
branch=None,
@@ -167,6 +160,8 @@ def test_run_ci_pipeline_fallback_to_default_branch(mock_subprocess, mock_gitlab
working_directory="/tmp"
)
assert result["success"] is True
+ assert result["pipeline_id"] == 789
+ assert result["pipeline_url"] == "https://gitlab.com/project/-/pipelines/789"
assert result["branch"] == "main"
proj.pipelines.create.assert_called_once()
@@ -206,4 +201,80 @@ def test_run_ci_pipeline_error_handling(mock_gitlab, mock_settings):
working_directory="/tmp"
)
assert "error" in result
- assert "Not found" in result["error"] \ No newline at end of file
+ assert "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_find_group(mock_gitlab, mock_settings):
+ server = GitLabPythonServer(working_directory="/tmp")
+ group1 = MagicMock()
+ group1.id = 1
+ group1.name = "Test Group"
+ group1.full_path = "test-group"
+ group1.web_url = "https://gitlab.com/groups/test-group"
+ group1.description = "A test group"
+ mock_gitlab.return_value.groups.list.return_value = [group1]
+ result = server.find_group("test-group")
+ assert isinstance(result, list)
+ assert result[0]["name"] == "Test Group"
+ assert result[0]["full_path"] == "test-group"
+
+@patch("mcp_server_gitlab_python.server.get_gitlab_settings", return_value=("https://gitlab.com", "dummy-token"))
+@patch("gitlab.Gitlab")
+def test_search_epics_success(mock_gitlab, mock_settings):
+ server = GitLabPythonServer(working_directory="/tmp")
+ group = MagicMock()
+ epic = MagicMock()
+ epic.id = 1
+ epic.iid = 101
+ epic.title = "Epic Title"
+ epic.web_url = "https://gitlab.com/groups/test-group/-/epics/101"
+ epic.state = "opened"
+ epic.created_at = "2024-01-01T00:00:00Z"
+ epic.updated_at = "2024-01-02T00:00:00Z"
+ epic.author = {"id": 2, "name": "Author"}
+ epic.labels = ["label1"]
+ epic.description = "Epic description"
+ group.epics.list.return_value = [epic]
+ mock_gitlab.return_value.groups.get.return_value = group
+ result = server.search_epics("test-group", state="opened")
+ assert "epics" in result
+ assert result["epics"][0]["title"] == "Epic Title"
+ assert result["epics"][0]["state"] == "opened"
+ assert result["epics"][0]["author"]["name"] == "Author"
+
+@patch("mcp_server_gitlab_python.server.get_gitlab_settings", return_value=("https://gitlab.com", "dummy-token"))
+@patch("gitlab.Gitlab")
+def test_search_epics_error(mock_gitlab, mock_settings):
+ server = GitLabPythonServer(working_directory="/tmp")
+ mock_gitlab.return_value.groups.get.side_effect = Exception("Group not found")
+ result = server.search_epics("bad-group")
+ 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_success(mock_gitlab, mock_settings):
+ server = GitLabPythonServer(working_directory="/tmp")
+ group = MagicMock()
+ epic = MagicMock()
+ epic.web_url = "https://gitlab.com/groups/test-group/-/epics/101"
+ group.epics.create.return_value = epic
+ mock_gitlab.return_value.groups.get.return_value = group
+ result = server.create_epic("test-group", "Epic Title", "Epic description", labels=["label1"])
+ assert "url" in result
+ assert result["url"] == "https://gitlab.com/groups/test-group/-/epics/101"
+ group.epics.create.assert_called_once()
+ args = group.epics.create.call_args[0][0]
+ assert args["title"] == "Epic Title"
+ assert args["description"] == "Epic description"
+ assert args["labels"] == ["label1"]
+
+@patch("mcp_server_gitlab_python.server.get_gitlab_settings", return_value=("https://gitlab.com", "dummy-token"))
+@patch("gitlab.Gitlab")
+def test_create_epic_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("bad-group", "Epic Title", "Epic description")
+ assert "error" in result
+ assert "Group not found" in result["error"] \ No newline at end of file
diff --git a/servers/gitlab_python/uv.lock b/servers/gitlab_python/uv.lock
index b1c296e..5601b46 100644
--- a/servers/gitlab_python/uv.lock
+++ b/servers/gitlab_python/uv.lock
@@ -172,6 +172,30 @@ toml = [
]
[[package]]
+name = "gitdb"
+version = "4.0.12"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "smmap" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 },
+]
+
+[[package]]
+name = "gitpython"
+version = "3.1.44"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "gitdb" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599 },
+]
+
+[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
@@ -289,6 +313,7 @@ name = "mcp-server-gitlab-python"
version = "0.1.0"
source = { editable = "." }
dependencies = [
+ { name = "gitpython" },
{ name = "mcp" },
{ name = "pydantic" },
{ name = "python-gitlab" },
@@ -304,9 +329,10 @@ dev = [
[package.metadata]
requires-dist = [
+ { name = "gitpython", specifier = ">=3.1.43" },
{ name = "mcp", specifier = ">=1.6.0" },
{ name = "pydantic", specifier = ">=2.11.0" },
- { name = "python-gitlab", specifier = ">=4.4.0" },
+ { name = "python-gitlab", specifier = ">=6.1.0" },
{ name = "pyyaml", specifier = ">=6.0.0" },
]
@@ -708,6 +734,15 @@ wheels = [
]
[[package]]
+name = "smmap"
+version = "5.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 },
+]
+
+[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }