Files
syndarix/mcp-servers/git-ops/exceptions.py
Felipe Cardoso 9dfa76aa41 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>
2026-01-06 20:48:20 +01:00

362 lines
9.6 KiB
Python

"""
Exception hierarchy for Git Operations MCP Server.
Provides structured error handling with error codes for MCP responses.
"""
from enum import Enum
from typing import Any
class ErrorCode(str, Enum):
"""Error codes for Git Operations errors."""
# General errors (1xxx)
INTERNAL_ERROR = "GIT_1000"
INVALID_REQUEST = "GIT_1001"
NOT_FOUND = "GIT_1002"
PERMISSION_DENIED = "GIT_1003"
TIMEOUT = "GIT_1004"
RATE_LIMITED = "GIT_1005"
# Workspace errors (2xxx)
WORKSPACE_NOT_FOUND = "GIT_2000"
WORKSPACE_LOCKED = "GIT_2001"
WORKSPACE_SIZE_EXCEEDED = "GIT_2002"
WORKSPACE_CREATE_FAILED = "GIT_2003"
WORKSPACE_DELETE_FAILED = "GIT_2004"
# Git operation errors (3xxx)
CLONE_FAILED = "GIT_3000"
CHECKOUT_FAILED = "GIT_3001"
COMMIT_FAILED = "GIT_3002"
PUSH_FAILED = "GIT_3003"
PULL_FAILED = "GIT_3004"
MERGE_CONFLICT = "GIT_3005"
BRANCH_EXISTS = "GIT_3006"
BRANCH_NOT_FOUND = "GIT_3007"
INVALID_REF = "GIT_3008"
DIRTY_WORKSPACE = "GIT_3009"
UNCOMMITTED_CHANGES = "GIT_3010"
FETCH_FAILED = "GIT_3011"
RESET_FAILED = "GIT_3012"
# Provider errors (4xxx)
PROVIDER_ERROR = "GIT_4000"
PROVIDER_AUTH_FAILED = "GIT_4001"
PROVIDER_NOT_FOUND = "GIT_4002"
PR_CREATE_FAILED = "GIT_4003"
PR_MERGE_FAILED = "GIT_4004"
PR_NOT_FOUND = "GIT_4005"
API_ERROR = "GIT_4006"
# Credential errors (5xxx)
CREDENTIAL_ERROR = "GIT_5000"
CREDENTIAL_NOT_FOUND = "GIT_5001"
CREDENTIAL_INVALID = "GIT_5002"
SSH_KEY_ERROR = "GIT_5003"
class GitOpsError(Exception):
"""Base exception for Git Operations errors."""
def __init__(
self,
message: str,
code: ErrorCode = ErrorCode.INTERNAL_ERROR,
details: dict[str, Any] | None = None,
) -> None:
super().__init__(message)
self.message = message
self.code = code
self.details = details or {}
def to_dict(self) -> dict[str, Any]:
"""Convert to dictionary for MCP response."""
result = {
"error": self.message,
"code": self.code.value,
}
if self.details:
result["details"] = self.details
return result
# Workspace Errors
class WorkspaceError(GitOpsError):
"""Base exception for workspace-related errors."""
def __init__(
self,
message: str,
code: ErrorCode = ErrorCode.WORKSPACE_NOT_FOUND,
details: dict[str, Any] | None = None,
) -> None:
super().__init__(message, code, details)
class WorkspaceNotFoundError(WorkspaceError):
"""Workspace does not exist."""
def __init__(self, project_id: str) -> None:
super().__init__(
f"Workspace not found for project: {project_id}",
ErrorCode.WORKSPACE_NOT_FOUND,
{"project_id": project_id},
)
class WorkspaceLockedError(WorkspaceError):
"""Workspace is locked by another operation."""
def __init__(self, project_id: str, holder: str | None = None) -> None:
details: dict[str, Any] = {"project_id": project_id}
if holder:
details["locked_by"] = holder
super().__init__(
f"Workspace is locked for project: {project_id}",
ErrorCode.WORKSPACE_LOCKED,
details,
)
class WorkspaceSizeExceededError(WorkspaceError):
"""Workspace size limit exceeded."""
def __init__(self, project_id: str, current_size: float, max_size: float) -> None:
super().__init__(
f"Workspace size limit exceeded for project: {project_id}",
ErrorCode.WORKSPACE_SIZE_EXCEEDED,
{
"project_id": project_id,
"current_size_gb": current_size,
"max_size_gb": max_size,
},
)
# Git Operation Errors
class GitError(GitOpsError):
"""Base exception for git operation errors."""
def __init__(
self,
message: str,
code: ErrorCode = ErrorCode.INTERNAL_ERROR,
details: dict[str, Any] | None = None,
) -> None:
super().__init__(message, code, details)
class CloneError(GitError):
"""Failed to clone repository."""
def __init__(self, repo_url: str, reason: str) -> None:
super().__init__(
f"Failed to clone repository: {reason}",
ErrorCode.CLONE_FAILED,
{"repo_url": repo_url, "reason": reason},
)
class CheckoutError(GitError):
"""Failed to checkout branch or ref."""
def __init__(self, ref: str, reason: str) -> None:
super().__init__(
f"Failed to checkout '{ref}': {reason}",
ErrorCode.CHECKOUT_FAILED,
{"ref": ref, "reason": reason},
)
class CommitError(GitError):
"""Failed to commit changes."""
def __init__(self, reason: str) -> None:
super().__init__(
f"Failed to commit: {reason}",
ErrorCode.COMMIT_FAILED,
{"reason": reason},
)
class PushError(GitError):
"""Failed to push to remote."""
def __init__(self, branch: str, reason: str) -> None:
super().__init__(
f"Failed to push branch '{branch}': {reason}",
ErrorCode.PUSH_FAILED,
{"branch": branch, "reason": reason},
)
class PullError(GitError):
"""Failed to pull from remote."""
def __init__(self, branch: str, reason: str) -> None:
super().__init__(
f"Failed to pull branch '{branch}': {reason}",
ErrorCode.PULL_FAILED,
{"branch": branch, "reason": reason},
)
class MergeConflictError(GitError):
"""Merge conflict detected."""
def __init__(self, conflicting_files: list[str]) -> None:
super().__init__(
f"Merge conflict detected in {len(conflicting_files)} files",
ErrorCode.MERGE_CONFLICT,
{"conflicting_files": conflicting_files},
)
class BranchExistsError(GitError):
"""Branch already exists."""
def __init__(self, branch_name: str) -> None:
super().__init__(
f"Branch already exists: {branch_name}",
ErrorCode.BRANCH_EXISTS,
{"branch": branch_name},
)
class BranchNotFoundError(GitError):
"""Branch does not exist."""
def __init__(self, branch_name: str) -> None:
super().__init__(
f"Branch not found: {branch_name}",
ErrorCode.BRANCH_NOT_FOUND,
{"branch": branch_name},
)
class InvalidRefError(GitError):
"""Invalid git reference."""
def __init__(self, ref: str) -> None:
super().__init__(
f"Invalid git reference: {ref}",
ErrorCode.INVALID_REF,
{"ref": ref},
)
class DirtyWorkspaceError(GitError):
"""Workspace has uncommitted changes."""
def __init__(self, modified_files: list[str]) -> None:
super().__init__(
f"Workspace has {len(modified_files)} uncommitted changes",
ErrorCode.DIRTY_WORKSPACE,
{"modified_files": modified_files[:10]}, # Limit to first 10
)
# Provider Errors
class ProviderError(GitOpsError):
"""Base exception for provider-related errors."""
def __init__(
self,
message: str,
code: ErrorCode = ErrorCode.PROVIDER_ERROR,
details: dict[str, Any] | None = None,
) -> None:
super().__init__(message, code, details)
class AuthenticationError(ProviderError):
"""Authentication with provider failed."""
def __init__(self, provider: str, reason: str) -> None:
super().__init__(
f"Authentication failed with {provider}: {reason}",
ErrorCode.PROVIDER_AUTH_FAILED,
{"provider": provider, "reason": reason},
)
class ProviderNotFoundError(ProviderError):
"""Provider not configured or recognized."""
def __init__(self, provider: str) -> None:
super().__init__(
f"Provider not found or not configured: {provider}",
ErrorCode.PROVIDER_NOT_FOUND,
{"provider": provider},
)
class PRError(ProviderError):
"""Pull request operation failed."""
def __init__(
self,
message: str,
code: ErrorCode = ErrorCode.PR_CREATE_FAILED,
details: dict[str, Any] | None = None,
) -> None:
super().__init__(message, code, details)
class PRNotFoundError(PRError):
"""Pull request not found."""
def __init__(self, pr_number: int, repo: str) -> None:
super().__init__(
f"Pull request #{pr_number} not found in {repo}",
ErrorCode.PR_NOT_FOUND,
{"pr_number": pr_number, "repo": repo},
)
class APIError(ProviderError):
"""Provider API error."""
def __init__(
self, provider: str, status_code: int, message: str
) -> None:
super().__init__(
f"{provider} API error ({status_code}): {message}",
ErrorCode.API_ERROR,
{"provider": provider, "status_code": status_code, "message": message},
)
# Credential Errors
class CredentialError(GitOpsError):
"""Base exception for credential-related errors."""
def __init__(
self,
message: str,
code: ErrorCode = ErrorCode.CREDENTIAL_ERROR,
details: dict[str, Any] | None = None,
) -> None:
super().__init__(message, code, details)
class CredentialNotFoundError(CredentialError):
"""Credential not found."""
def __init__(self, credential_type: str, identifier: str) -> None:
super().__init__(
f"{credential_type} credential not found: {identifier}",
ErrorCode.CREDENTIAL_NOT_FOUND,
{"type": credential_type, "identifier": identifier},
)