forked from cardosofelipe/fast-next-template
- Added `mcp-git-ops` service to `docker-compose.dev.yml` with health checks and configurations. - Integrated SSRF protection in repository URL validation for enhanced security. - Expanded `pyproject.toml` mypy settings and adjusted code to meet stricter type checking. - Improved workspace management and GitWrapper operations with error handling refinements. - Updated input validation, branching, and repository operations to align with new error structure. - Shut down thread pool executor gracefully during server cleanup.
360 lines
9.6 KiB
Python
360 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: dict[str, Any] = {
|
|
"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},
|
|
)
|