forked from cardosofelipe/fast-next-template
feat(mcp): implement Git Operations MCP server with Gitea provider
Implements the Git Operations MCP server (Issue #58) providing: Core features: - GitPython wrapper for local repository operations (clone, commit, push, pull, diff, log) - Branch management (create, delete, list, checkout) - Workspace isolation per project with file-based locking - Gitea provider for remote PR operations MCP Tools (17 registered): - clone_repository, git_status, create_branch, list_branches - checkout, commit, push, pull, diff, log - create_pull_request, get_pull_request, list_pull_requests - merge_pull_request, get_workspace, lock_workspace, unlock_workspace Technical details: - FastMCP + FastAPI with JSON-RPC 2.0 protocol - pydantic-settings for configuration (env prefix: GIT_OPS_) - Comprehensive error hierarchy with structured codes - 131 tests passing with 67% coverage - Async operations via ThreadPoolExecutor Closes: #105, #106, #107, #108, #109 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
388
mcp-servers/git-ops/providers/base.py
Normal file
388
mcp-servers/git-ops/providers/base.py
Normal file
@@ -0,0 +1,388 @@
|
||||
"""
|
||||
Base provider interface for git hosting platforms.
|
||||
|
||||
Defines the abstract interface that all git providers must implement.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
|
||||
from models import (
|
||||
CreatePRResult,
|
||||
GetPRResult,
|
||||
ListPRsResult,
|
||||
MergePRResult,
|
||||
MergeStrategy,
|
||||
PRState,
|
||||
UpdatePRResult,
|
||||
)
|
||||
|
||||
|
||||
class BaseProvider(ABC):
|
||||
"""
|
||||
Abstract base class for git hosting providers.
|
||||
|
||||
All providers (Gitea, GitHub, GitLab) must implement this interface.
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def name(self) -> str:
|
||||
"""Return the provider name (e.g., 'gitea', 'github')."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def is_connected(self) -> bool:
|
||||
"""Check if the provider is connected and authenticated."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def get_authenticated_user(self) -> str | None:
|
||||
"""Get the username of the authenticated user."""
|
||||
...
|
||||
|
||||
# Repository operations
|
||||
|
||||
@abstractmethod
|
||||
async def get_repo_info(
|
||||
self, owner: str, repo: str
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Get repository information.
|
||||
|
||||
Args:
|
||||
owner: Repository owner/organization
|
||||
repo: Repository name
|
||||
|
||||
Returns:
|
||||
Repository info dict
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def get_default_branch(
|
||||
self, owner: str, repo: str
|
||||
) -> str:
|
||||
"""
|
||||
Get the default branch for a repository.
|
||||
|
||||
Args:
|
||||
owner: Repository owner/organization
|
||||
repo: Repository name
|
||||
|
||||
Returns:
|
||||
Default branch name
|
||||
"""
|
||||
...
|
||||
|
||||
# Pull Request operations
|
||||
|
||||
@abstractmethod
|
||||
async def create_pr(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
title: str,
|
||||
body: str,
|
||||
source_branch: str,
|
||||
target_branch: str,
|
||||
draft: bool = False,
|
||||
labels: list[str] | None = None,
|
||||
assignees: list[str] | None = None,
|
||||
reviewers: list[str] | None = None,
|
||||
) -> CreatePRResult:
|
||||
"""
|
||||
Create a pull request.
|
||||
|
||||
Args:
|
||||
owner: Repository owner
|
||||
repo: Repository name
|
||||
title: PR title
|
||||
body: PR description
|
||||
source_branch: Source branch name
|
||||
target_branch: Target branch name
|
||||
draft: Whether to create as draft
|
||||
labels: Labels to add
|
||||
assignees: Users to assign
|
||||
reviewers: Users to request review from
|
||||
|
||||
Returns:
|
||||
CreatePRResult with PR number and URL
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def get_pr(
|
||||
self, owner: str, repo: str, pr_number: int
|
||||
) -> GetPRResult:
|
||||
"""
|
||||
Get a pull request by number.
|
||||
|
||||
Args:
|
||||
owner: Repository owner
|
||||
repo: Repository name
|
||||
pr_number: Pull request number
|
||||
|
||||
Returns:
|
||||
GetPRResult with PR details
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def list_prs(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
state: PRState | None = None,
|
||||
author: str | None = None,
|
||||
limit: int = 20,
|
||||
) -> ListPRsResult:
|
||||
"""
|
||||
List pull requests.
|
||||
|
||||
Args:
|
||||
owner: Repository owner
|
||||
repo: Repository name
|
||||
state: Filter by state (open, closed, merged)
|
||||
author: Filter by author
|
||||
limit: Maximum PRs to return
|
||||
|
||||
Returns:
|
||||
ListPRsResult with list of PRs
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def merge_pr(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
pr_number: int,
|
||||
merge_strategy: MergeStrategy = MergeStrategy.MERGE,
|
||||
commit_message: str | None = None,
|
||||
delete_branch: bool = True,
|
||||
) -> MergePRResult:
|
||||
"""
|
||||
Merge a pull request.
|
||||
|
||||
Args:
|
||||
owner: Repository owner
|
||||
repo: Repository name
|
||||
pr_number: Pull request number
|
||||
merge_strategy: Merge strategy to use
|
||||
commit_message: Custom merge commit message
|
||||
delete_branch: Whether to delete source branch
|
||||
|
||||
Returns:
|
||||
MergePRResult with merge status
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def update_pr(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
pr_number: int,
|
||||
title: str | None = None,
|
||||
body: str | None = None,
|
||||
state: PRState | None = None,
|
||||
labels: list[str] | None = None,
|
||||
assignees: list[str] | None = None,
|
||||
) -> UpdatePRResult:
|
||||
"""
|
||||
Update a pull request.
|
||||
|
||||
Args:
|
||||
owner: Repository owner
|
||||
repo: Repository name
|
||||
pr_number: Pull request number
|
||||
title: New title
|
||||
body: New description
|
||||
state: New state (open, closed)
|
||||
labels: Replace labels
|
||||
assignees: Replace assignees
|
||||
|
||||
Returns:
|
||||
UpdatePRResult with updated PR info
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def close_pr(
|
||||
self, owner: str, repo: str, pr_number: int
|
||||
) -> UpdatePRResult:
|
||||
"""
|
||||
Close a pull request without merging.
|
||||
|
||||
Args:
|
||||
owner: Repository owner
|
||||
repo: Repository name
|
||||
pr_number: Pull request number
|
||||
|
||||
Returns:
|
||||
UpdatePRResult with updated PR info
|
||||
"""
|
||||
...
|
||||
|
||||
# Branch operations via API (for operations that need to bypass local git)
|
||||
|
||||
@abstractmethod
|
||||
async def delete_remote_branch(
|
||||
self, owner: str, repo: str, branch: str
|
||||
) -> bool:
|
||||
"""
|
||||
Delete a remote branch via API.
|
||||
|
||||
Args:
|
||||
owner: Repository owner
|
||||
repo: Repository name
|
||||
branch: Branch name to delete
|
||||
|
||||
Returns:
|
||||
True if deleted, False otherwise
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def get_branch(
|
||||
self, owner: str, repo: str, branch: str
|
||||
) -> dict[str, Any] | None:
|
||||
"""
|
||||
Get branch information via API.
|
||||
|
||||
Args:
|
||||
owner: Repository owner
|
||||
repo: Repository name
|
||||
branch: Branch name
|
||||
|
||||
Returns:
|
||||
Branch info dict or None if not found
|
||||
"""
|
||||
...
|
||||
|
||||
# Comment operations
|
||||
|
||||
@abstractmethod
|
||||
async def add_pr_comment(
|
||||
self, owner: str, repo: str, pr_number: int, body: str
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Add a comment to a pull request.
|
||||
|
||||
Args:
|
||||
owner: Repository owner
|
||||
repo: Repository name
|
||||
pr_number: Pull request number
|
||||
body: Comment body
|
||||
|
||||
Returns:
|
||||
Created comment info
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def list_pr_comments(
|
||||
self, owner: str, repo: str, pr_number: int
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
List comments on a pull request.
|
||||
|
||||
Args:
|
||||
owner: Repository owner
|
||||
repo: Repository name
|
||||
pr_number: Pull request number
|
||||
|
||||
Returns:
|
||||
List of comments
|
||||
"""
|
||||
...
|
||||
|
||||
# Label operations
|
||||
|
||||
@abstractmethod
|
||||
async def add_labels(
|
||||
self, owner: str, repo: str, pr_number: int, labels: list[str]
|
||||
) -> list[str]:
|
||||
"""
|
||||
Add labels to a pull request.
|
||||
|
||||
Args:
|
||||
owner: Repository owner
|
||||
repo: Repository name
|
||||
pr_number: Pull request number
|
||||
labels: Labels to add
|
||||
|
||||
Returns:
|
||||
Updated list of labels
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def remove_label(
|
||||
self, owner: str, repo: str, pr_number: int, label: str
|
||||
) -> list[str]:
|
||||
"""
|
||||
Remove a label from a pull request.
|
||||
|
||||
Args:
|
||||
owner: Repository owner
|
||||
repo: Repository name
|
||||
pr_number: Pull request number
|
||||
label: Label to remove
|
||||
|
||||
Returns:
|
||||
Updated list of labels
|
||||
"""
|
||||
...
|
||||
|
||||
# Reviewer operations
|
||||
|
||||
@abstractmethod
|
||||
async def request_review(
|
||||
self, owner: str, repo: str, pr_number: int, reviewers: list[str]
|
||||
) -> list[str]:
|
||||
"""
|
||||
Request review from users.
|
||||
|
||||
Args:
|
||||
owner: Repository owner
|
||||
repo: Repository name
|
||||
pr_number: Pull request number
|
||||
reviewers: Usernames to request review from
|
||||
|
||||
Returns:
|
||||
List of reviewers requested
|
||||
"""
|
||||
...
|
||||
|
||||
# Utility methods
|
||||
|
||||
def parse_repo_url(self, repo_url: str) -> tuple[str, str]:
|
||||
"""
|
||||
Parse repository URL to extract owner and repo name.
|
||||
|
||||
Args:
|
||||
repo_url: Repository URL (HTTPS or SSH)
|
||||
|
||||
Returns:
|
||||
Tuple of (owner, repo)
|
||||
|
||||
Raises:
|
||||
ValueError: If URL cannot be parsed
|
||||
"""
|
||||
import re
|
||||
|
||||
# Handle SSH URLs: git@host:owner/repo.git
|
||||
ssh_match = re.match(r"git@[^:]+:([^/]+)/([^/]+?)(?:\.git)?$", repo_url)
|
||||
if ssh_match:
|
||||
return ssh_match.group(1), ssh_match.group(2)
|
||||
|
||||
# Handle HTTPS URLs: https://host/owner/repo.git
|
||||
https_match = re.match(
|
||||
r"https?://[^/]+/([^/]+)/([^/]+?)(?:\.git)?$", repo_url
|
||||
)
|
||||
if https_match:
|
||||
return https_match.group(1), https_match.group(2)
|
||||
|
||||
raise ValueError(f"Unable to parse repository URL: {repo_url}")
|
||||
Reference in New Issue
Block a user