Files
syndarix/mcp-servers/git-ops/exceptions.py
Felipe Cardoso 76d7de5334 **feat(git-ops): enhance MCP server with Git provider updates and SSRF protection**
- 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.
2026-01-07 09:17:00 +01:00

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},
)