"""MCP configuration types with pydantic models for runtime validation.""" from __future__ import annotations from dataclasses import dataclass, field from typing import Any, Literal, Optional import pydantic # ── Legacy Dataclasses (backward compatible) ── @dataclass(frozen=True) class McpToolPolicy: allow: tuple[str, ...] = () deny: tuple[str, ...] = () @dataclass(frozen=True) class McpServerConfig: server_id: str transport: Literal["stdio", "http"] timeout_seconds: float = 60.0 command: str = "" args: tuple[str, ...] = () env: dict[str, str] = field(default_factory=dict) cwd: str = "" url: str = "" headers: dict[str, str] = field(default_factory=dict) sse: bool = False hosts_allow: tuple[str, ...] = () tools: McpToolPolicy = field(default_factory=McpToolPolicy) classify: dict[str, str] = field(default_factory=dict) @dataclass class McpToolRef: server_id: str original_name: str registered_name: str classification: str # readonly | mutating | network input_schema: dict[str, Any] description: str = "" @dataclass class McpServerStatus: server_id: str transport: str connected: bool = False tool_count: int = 0 last_error: str = "" degraded: bool = False # ── Pydantic Models (new runtime validation) ── class McpRequest(pydantic.BaseModel): # type: ignore[misc] """MCP request message.""" jsonrpc: str = "2.0" id: str | int method: str params: dict[str, Any] = pydantic.Field(default_factory=dict) class McpResponse(pydantic.BaseModel): # type: ignore[misc] """MCP response message.""" jsonrpc: str = "2.0" id: str | int | None = None class McpSuccessResponse(McpResponse): """Successful MCP response.""" result: dict[str, Any] class McpError(pydantic.BaseModel): # type: ignore[misc] """MCP error object.""" code: int message: str data: Optional[Any] = None class McpErrorResponse(McpResponse): """Error MCP response.""" error: McpError class McpToolCall(pydantic.BaseModel): # type: ignore[misc] """MCP tool call request.""" name: str arguments: dict[str, Any] = pydantic.Field(default_factory=dict) class McpToolResult(pydantic.BaseModel): # type: ignore[misc] """MCP tool execution result.""" success: bool content: Any = None error: str = "" server_id: str = "" tool_name: str = "" duration_ms: float = 0.0 model_config = pydantic.ConfigDict(frozen=True) class McpCapability(pydantic.BaseModel): # type: ignore[misc] """MCP server capability.""" name: str version: str = "1.0" class McpToolDescription(pydantic.BaseModel): # type: ignore[misc] """MCP tool description.""" name: str description: str = "" inputSchema: dict[str, Any] = pydantic.Field(default_factory=dict) # ── Validation Functions ── def validate_mcp_request(data: dict[str, Any]) -> tuple[bool, McpRequest | dict[str, Any]]: """Validate MCP request data.""" try: return True, McpRequest(**data) except pydantic.ValidationError as e: errors = [] for error in e.errors(): loc = ".".join(str(l) for l in error.get("loc", [])) msg = error.get("msg", "") errors.append(f"{loc}: {msg}") return False, { "error": f"Invalid MCP request: {', '.join(errors)}", "code": "MCP_REQUEST_VALIDATION_FAILED", "validation_errors": errors, } def validate_mcp_response(data: dict[str, Any]) -> tuple[bool, McpResponse | dict[str, Any]]: """Validate MCP response data.""" try: if "error" in data: return True, McpErrorResponse(**data) elif "result" in data: return True, McpSuccessResponse(**data) return True, McpResponse(**data) except pydantic.ValidationError as e: errors = [] for error in e.errors(): loc = ".".join(str(l) for l in error.get("loc", [])) msg = error.get("msg", "") errors.append(f"{loc}: {msg}") return False, { "error": f"Invalid MCP response: {', '.join(errors)}", "code": "MCP_RESPONSE_VALIDATION_FAILED", "validation_errors": errors, } __all__ = [ "McpToolPolicy", "McpServerConfig", "McpToolRef", "McpServerStatus", "McpRequest", "McpResponse", "McpSuccessResponse", "McpErrorResponse", "McpError", "McpToolCall", "McpToolResult", "McpCapability", "McpToolDescription", "validate_mcp_request", "validate_mcp_response", ]