Skip to main content
Glama
container.py6.35 kB
"""Container-related data models.""" from datetime import datetime from typing import Annotated, Any, Literal from pydantic import BaseModel, Field, field_validator from .enums import ProtocolLiteral class MCPModel(BaseModel): """Base model with common MCP settings.""" def model_dump(self, **kwargs) -> dict[str, Any]: """Convert to dict with exclude_none by default.""" kwargs.setdefault("exclude_none", True) return super().model_dump(**kwargs) # Minimal Pydantic models for type safety (matches current dict shapes) class ContainerInfo(MCPModel): """Information about a Docker container (minimal for type safety).""" container_id: str name: str host_id: str image: str | None = None status: str | None = None state: str | None = None ports: list[str] = Field(default_factory=list) class ContainerStats(MCPModel): """Resource statistics for a container.""" container_id: str host_id: str cpu_percentage: float | None = None memory_usage: int | None = None # bytes memory_limit: int | None = None # bytes memory_percentage: float | None = None network_rx: int | None = None # bytes network_tx: int | None = None # bytes block_read: int | None = None # bytes block_write: int | None = None # bytes pids: int | None = None class ContainerLogs(MCPModel): """Container log data.""" container_id: str host_id: str logs: list[str] timestamp: datetime = Field(description="Log retrieval timestamp in ISO 8601 format") truncated: bool = False class StackInfo(MCPModel): """Information about a Docker Compose stack.""" name: str host_id: str services: list[str] = Field(default_factory=list) status: str created: datetime | None = Field( default=None, description="Creation timestamp in ISO 8601 format" ) updated: datetime | None = Field( default=None, description="Last update timestamp in ISO 8601 format" ) compose_file: str | None = None # Minimal request model for type safety class DeployStackRequest(MCPModel): """Request to deploy a Docker Compose stack (minimal for type safety).""" host_id: str stack_name: str compose_content: str environment: dict[str, str] = Field(default_factory=dict) pull_images: bool = True recreate: bool = False class ContainerActionRequest(MCPModel): """Request to perform an action on a container.""" host_id: str container_id: str action: Literal["start", "stop", "restart", "remove", "pause", "unpause"] force: bool = False class LogStreamRequest(MCPModel): """Request to stream container logs.""" host_id: str container_id: str follow: bool = True tail: int = 100 since: str | None = None timestamps: bool = False class PortMapping(MCPModel): """Individual port mapping with container context.""" host_id: str host_ip: str host_port: Annotated[int, Field(ge=1, le=65535, description="Host port number")] container_port: Annotated[int, Field(ge=1, le=65535, description="Container port number")] protocol: ProtocolLiteral container_id: str container_name: str image: str compose_project: str | None = None is_conflict: bool = False conflict_with: list[str] = Field(default_factory=list) @field_validator("protocol", mode="before") @classmethod def normalize_protocol(cls, v: str) -> str: """Normalize protocol casing to lowercase and validate.""" if not isinstance(v, str): raise ValueError(f"Protocol must be a string, got {type(v)}") normalized = v.strip().lower() if not normalized: raise ValueError("Protocol cannot be empty") # Validate against allowed protocols valid_protocols = {"tcp", "udp", "sctp"} if normalized not in valid_protocols: raise ValueError(f"Invalid protocol '{v}'. Must be one of: {', '.join(sorted(valid_protocols))}") return normalized @field_validator("host_port", "container_port", mode="before") @classmethod def parse_port_numbers(cls, v: str | int) -> int: """Parse and validate port numbers from strings or integers.""" if isinstance(v, int): return v if isinstance(v, str): # Strip whitespace and parse v = v.strip() if not v: raise ValueError("Port number cannot be empty") try: port = int(v) if not (1 <= port <= 65535): raise ValueError(f"Port {port} out of valid range 1-65535") return port except ValueError as e: if "invalid literal" in str(e): raise ValueError(f"Invalid port number: '{v}' (must be numeric)") from e raise raise ValueError(f"Port must be string or integer, got {type(v)}") class PortConflict(MCPModel): """Port conflict information.""" host_id: str host_port: str protocol: ProtocolLiteral host_ip: str affected_containers: list[str] container_details: list[dict[str, Any]] = Field(default_factory=list) @field_validator("protocol", mode="before") @classmethod def normalize_protocol(cls, v: str) -> str: """Normalize protocol casing to lowercase and validate.""" if not isinstance(v, str): raise ValueError(f"Protocol must be a string, got {type(v)}") normalized = v.strip().lower() if not normalized: raise ValueError("Protocol cannot be empty") # Validate against allowed protocols valid_protocols = {"tcp", "udp", "sctp"} if normalized not in valid_protocols: raise ValueError(f"Invalid protocol '{v}'. Must be one of: {', '.join(sorted(valid_protocols))}") return normalized class PortListResponse(MCPModel): """Port listing response (minimal for type safety).""" host_id: str total_ports: int total_containers: int port_mappings: list[PortMapping] = Field(default_factory=list) conflicts: list[PortConflict] = Field(default_factory=list) summary: dict[str, Any] = Field(default_factory=dict) timestamp: str | None = None # ISO 8601 format

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jmagar/docker-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server