forked from cardosofelipe/fast-next-template
Core MCP client implementation with comprehensive tooling:
**Services:**
- MCPClientManager: Main facade for all MCP operations
- MCPServerRegistry: Thread-safe singleton for server configs
- ConnectionPool: Connection pooling with auto-reconnection
- ToolRouter: Automatic tool routing with circuit breaker
- AsyncCircuitBreaker: Custom async-compatible circuit breaker
**Configuration:**
- YAML-based config with Pydantic models
- Environment variable expansion support
- Transport types: HTTP, SSE, STDIO
**API Endpoints:**
- GET /mcp/servers - List all MCP servers
- GET /mcp/servers/{name}/tools - List server tools
- GET /mcp/tools - List all tools from all servers
- GET /mcp/health - Health check all servers
- POST /mcp/call - Execute tool (admin only)
- GET /mcp/circuit-breakers - Circuit breaker status
- POST /mcp/circuit-breakers/{name}/reset - Reset circuit breaker
- POST /mcp/servers/{name}/reconnect - Force reconnection
**Testing:**
- 156 unit tests with comprehensive coverage
- Tests for all services, routes, and error handling
- Proper mocking and async test support
**Documentation:**
- MCP_CLIENT.md with usage examples
- Phase 2+ workflow documentation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
202 lines
5.9 KiB
Python
202 lines
5.9 KiB
Python
"""
|
|
MCP Exception Classes
|
|
|
|
Custom exceptions for MCP client operations with detailed error context.
|
|
"""
|
|
|
|
from typing import Any
|
|
|
|
|
|
class MCPError(Exception):
|
|
"""Base exception for all MCP-related errors."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
*,
|
|
server_name: str | None = None,
|
|
details: dict[str, Any] | None = None,
|
|
) -> None:
|
|
super().__init__(message)
|
|
self.message = message
|
|
self.server_name = server_name
|
|
self.details = details or {}
|
|
|
|
def __str__(self) -> str:
|
|
parts = [self.message]
|
|
if self.server_name:
|
|
parts.append(f"server={self.server_name}")
|
|
if self.details:
|
|
parts.append(f"details={self.details}")
|
|
return " | ".join(parts)
|
|
|
|
|
|
class MCPConnectionError(MCPError):
|
|
"""Raised when connection to an MCP server fails."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
*,
|
|
server_name: str | None = None,
|
|
url: str | None = None,
|
|
cause: Exception | None = None,
|
|
details: dict[str, Any] | None = None,
|
|
) -> None:
|
|
super().__init__(message, server_name=server_name, details=details)
|
|
self.url = url
|
|
self.cause = cause
|
|
|
|
def __str__(self) -> str:
|
|
base = super().__str__()
|
|
if self.url:
|
|
base = f"{base} | url={self.url}"
|
|
if self.cause:
|
|
base = f"{base} | cause={type(self.cause).__name__}: {self.cause}"
|
|
return base
|
|
|
|
|
|
class MCPTimeoutError(MCPError):
|
|
"""Raised when an MCP operation times out."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
*,
|
|
server_name: str | None = None,
|
|
timeout_seconds: float | None = None,
|
|
operation: str | None = None,
|
|
details: dict[str, Any] | None = None,
|
|
) -> None:
|
|
super().__init__(message, server_name=server_name, details=details)
|
|
self.timeout_seconds = timeout_seconds
|
|
self.operation = operation
|
|
|
|
def __str__(self) -> str:
|
|
base = super().__str__()
|
|
if self.timeout_seconds is not None:
|
|
base = f"{base} | timeout={self.timeout_seconds}s"
|
|
if self.operation:
|
|
base = f"{base} | operation={self.operation}"
|
|
return base
|
|
|
|
|
|
class MCPToolError(MCPError):
|
|
"""Raised when a tool execution fails."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
*,
|
|
server_name: str | None = None,
|
|
tool_name: str | None = None,
|
|
tool_args: dict[str, Any] | None = None,
|
|
error_code: str | None = None,
|
|
details: dict[str, Any] | None = None,
|
|
) -> None:
|
|
super().__init__(message, server_name=server_name, details=details)
|
|
self.tool_name = tool_name
|
|
self.tool_args = tool_args
|
|
self.error_code = error_code
|
|
|
|
def __str__(self) -> str:
|
|
base = super().__str__()
|
|
if self.tool_name:
|
|
base = f"{base} | tool={self.tool_name}"
|
|
if self.error_code:
|
|
base = f"{base} | error_code={self.error_code}"
|
|
return base
|
|
|
|
|
|
class MCPServerNotFoundError(MCPError):
|
|
"""Raised when a requested MCP server is not registered."""
|
|
|
|
def __init__(
|
|
self,
|
|
server_name: str,
|
|
*,
|
|
available_servers: list[str] | None = None,
|
|
details: dict[str, Any] | None = None,
|
|
) -> None:
|
|
message = f"MCP server not found: {server_name}"
|
|
super().__init__(message, server_name=server_name, details=details)
|
|
self.available_servers = available_servers or []
|
|
|
|
def __str__(self) -> str:
|
|
base = super().__str__()
|
|
if self.available_servers:
|
|
base = f"{base} | available={self.available_servers}"
|
|
return base
|
|
|
|
|
|
class MCPToolNotFoundError(MCPError):
|
|
"""Raised when a requested tool is not found on any server."""
|
|
|
|
def __init__(
|
|
self,
|
|
tool_name: str,
|
|
*,
|
|
server_name: str | None = None,
|
|
available_tools: list[str] | None = None,
|
|
details: dict[str, Any] | None = None,
|
|
) -> None:
|
|
message = f"Tool not found: {tool_name}"
|
|
super().__init__(message, server_name=server_name, details=details)
|
|
self.tool_name = tool_name
|
|
self.available_tools = available_tools or []
|
|
|
|
def __str__(self) -> str:
|
|
base = super().__str__()
|
|
if self.available_tools:
|
|
base = f"{base} | available_tools={self.available_tools[:5]}..."
|
|
return base
|
|
|
|
|
|
class MCPCircuitOpenError(MCPError):
|
|
"""Raised when a circuit breaker is open (server temporarily unavailable)."""
|
|
|
|
def __init__(
|
|
self,
|
|
server_name: str,
|
|
*,
|
|
failure_count: int | None = None,
|
|
reset_timeout: float | None = None,
|
|
details: dict[str, Any] | None = None,
|
|
) -> None:
|
|
message = f"Circuit breaker open for server: {server_name}"
|
|
super().__init__(message, server_name=server_name, details=details)
|
|
self.failure_count = failure_count
|
|
self.reset_timeout = reset_timeout
|
|
|
|
def __str__(self) -> str:
|
|
base = super().__str__()
|
|
if self.failure_count is not None:
|
|
base = f"{base} | failures={self.failure_count}"
|
|
if self.reset_timeout is not None:
|
|
base = f"{base} | reset_in={self.reset_timeout}s"
|
|
return base
|
|
|
|
|
|
class MCPValidationError(MCPError):
|
|
"""Raised when tool arguments fail validation."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
*,
|
|
tool_name: str | None = None,
|
|
field_errors: dict[str, str] | None = None,
|
|
details: dict[str, Any] | None = None,
|
|
) -> None:
|
|
super().__init__(message, details=details)
|
|
self.tool_name = tool_name
|
|
self.field_errors = field_errors or {}
|
|
|
|
def __str__(self) -> str:
|
|
base = super().__str__()
|
|
if self.tool_name:
|
|
base = f"{base} | tool={self.tool_name}"
|
|
if self.field_errors:
|
|
base = f"{base} | fields={list(self.field_errors.keys())}"
|
|
return base
|