From db86d27546cf0bcbbeb5f47be249fa0bae922204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B8=D0=BA=D0=BE=D0=BB=D0=B0=D0=B9?= Date: Fri, 19 Dec 2025 13:25:30 +0300 Subject: [PATCH 1/2] fix: resolve validation error "array type must have items" by replacing tuple with list in ports schema (Issue #25) --- src/mcp_server_docker/input_schemas.py | 106 ++++++++++++------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/src/mcp_server_docker/input_schemas.py b/src/mcp_server_docker/input_schemas.py index c912d0e..23af59e 100644 --- a/src/mcp_server_docker/input_schemas.py +++ b/src/mcp_server_docker/input_schemas.py @@ -13,16 +13,16 @@ class JSONParsingModel(BaseModel): - """ + \"\"\" A base Pydantic model that attempts to parse JSON strings for non-primitive fields. If a string is provided for a field that expects a complex type (dict, list, or another model), it will attempt to parse it as JSON. Claude appears to not understand that a nested field shouldn't be a JSON-encoded string... But it does send valid JSON! - """ + \"\"\" - @field_validator("*", mode="before") + @field_validator(\"*\", mode=\"before\") @classmethod def _try_parse_json(cls, value: Any, info: ValidationInfo): if not isinstance(value, str): @@ -57,63 +57,63 @@ def _try_parse_json(cls, value: Any, info: ValidationInfo): class FetchContainerLogsInput(JSONParsingModel): - container_id: str = Field(..., description="Container ID or name") - tail: int | Literal["all"] = Field( - 100, description="Number of lines to show from the end" + container_id: str = Field(..., description=\"Container ID or name\") + tail: int | Literal[\"all\"] = Field( + 100, description=\"Number of lines to show from the end\" ) class ListContainersFilters(JSONParsingModel): label: list[str] | None = Field( - None, description="Filter by label, either `key` or `key=value` format" + None, description=\"Filter by label, either `key` or `key=value` format\" ) class ListContainersInput(JSONParsingModel): all: bool = Field( - False, description="Show all containers (default shows just running)" + False, description=\"Show all containers (default shows just running)\" ) - filters: ListContainersFilters | None = Field(None, description="Filter containers") + filters: ListContainersFilters | None = Field(None, description=\"Filter containers\") class CreateContainerInput(JSONParsingModel): - """ + \"\"\" Schema for creating a new container. This is passed to the Python Docker SDK directly, so the fields are the same as the `docker.containers.create` method. - """ + \"\"\" detach: bool = Field( True, - description="Run container in the background. Should be True for long-running containers, can be false for short-lived containers", + description=\"Run container in the background. Should be True for long-running containers, can be false for short-lived containers\", ) - image: str = Field(..., description="Docker image name") - name: str | None = Field(None, description="Container name") - entrypoint: str | None = Field(None, description="Entrypoint to run in container") - command: str | None = Field(None, description="Command to run in container") - network: str | None = Field(None, description="Network to attach the container to") + image: str = Field(..., description=\"Docker image name\") + name: str | None = Field(None, description=\"Container name\") + entrypoint: str | None = Field(None, description=\"Entrypoint to run in container\") + command: str | None = Field(None, description=\"Command to run in container\") + network: str | None = Field(None, description=\"Network to attach the container to\") environment: dict[str, str] | None = Field( - None, description="Environment variables dictionary" + None, description=\"Environment variables dictionary\" ) - ports: dict[str, int | list[int] | tuple[str, int] | None] | None = Field( + ports: dict[str, int | list[int] | list[Any] | None] | None = Field( None, - description="A map whose keys are the container port, and the values are the host port(s) to bind to.", + description=\"A map whose keys are the container port, and the values are the host port(s) to bind to.\", ) volumes: dict[str, dict[str, str]] | list[str] | None = Field( - None, description="Volume mappings" + None, description=\"Volume mappings\" ) labels: dict[str, str] | list[str] | None = Field( None, - description="Container labels, either as a dictionary or a list of key=value strings", + description=\"Container labels, either as a dictionary or a list of key=value strings\", ) - auto_remove: bool = Field(False, description="Automatically remove the container") + auto_remove: bool = Field(False, description=\"Automatically remove the container\") class RecreateContainerInput(CreateContainerInput): container_id: str | None = Field( None, - description="Container ID to recreate. The `name` parameter will be used if this is not provided", + description=\"Container ID to recreate. The `name` parameter will be used if this is not provided\", ) @computed_field @@ -121,74 +121,74 @@ class RecreateContainerInput(CreateContainerInput): def resolved_container_id(self) -> str: return self.container_id or self.name # pyright: ignore - @model_validator(mode="after") + @model_validator(mode=\"after\") def validate_container_id(self): if self.container_id is None and self.name is None: raise ValueError( - "container_id or name is required for identifying the container to stop+remove" + \"container_id or name is required for identifying the container to stop+remove\" ) return self class ContainerActionInput(JSONParsingModel): - container_id: str = Field(..., description="Container ID or name") + container_id: str = Field(..., description=\"Container ID or name\") class RemoveContainerInput(JSONParsingModel): - container_id: str = Field(..., description="Container ID or name") - force: bool = Field(False, description="Force remove the container") + container_id: str = Field(..., description=\"Container ID or name\") + force: bool = Field(False, description=\"Force remove the container\") class ListImagesFilters(JSONParsingModel): - dangling: bool | None = Field(None, description="Show dangling images") + dangling: bool | None = Field(None, description=\"Show dangling images\") label: list[str] | None = Field( - None, description="Filter by label, either `key` or `key=value` format" + None, description=\"Filter by label, either `key` or `key=value` format\" ) class ListImagesInput(JSONParsingModel): name: str | None = Field( - None, description="Filter images by repository name, if desired" + None, description=\"Filter images by repository name, if desired\" ) - all: bool = Field(False, description="Show all images (default hides intermediate)") - filters: ListImagesFilters | None = Field(None, description="Filter images") + all: bool = Field(False, description=\"Show all images (default hides intermediate)\") + filters: ListImagesFilters | None = Field(None, description=\"Filter images\") class PullPushImageInput(JSONParsingModel): - repository: str = Field(..., description="Image repository") - tag: str | None = Field("latest", description="Image tag") + repository: str = Field(..., description=\"Image repository\") + tag: str | None = Field(\"latest\", description=\"Image tag\") class BuildImageInput(JSONParsingModel): - path: str = Field(..., description="Path to build context") - tag: str = Field(..., description="Image tag") - dockerfile: str | None = Field(None, description="Path to Dockerfile") + path: str = Field(..., description=\"Path to build context\") + tag: str = Field(..., description=\"Image tag\") + dockerfile: str | None = Field(None, description=\"Path to Dockerfile\") class RemoveImageInput(JSONParsingModel): - image: str = Field(..., description="Image ID or name") - force: bool = Field(False, description="Force remove the image") + image: str = Field(..., description=\"Image ID or name\") + force: bool = Field(False, description=\"Force remove the image\") class ListNetworksFilter(JSONParsingModel): label: list[str] | None = Field( - None, description="Filter by label, either `key` or `key=value` format" + None, description=\"Filter by label, either `key` or `key=value` format\" ) class ListNetworksInput(JSONParsingModel): - filters: ListNetworksFilter | None = Field(None, description="Filter networks") + filters: ListNetworksFilter | None = Field(None, description=\"Filter networks\") class CreateNetworkInput(JSONParsingModel): - name: str = Field(..., description="Network name") - driver: str | None = Field("bridge", description="Network driver") - internal: bool = Field(False, description="Create an internal network") - labels: dict[str, str] | None = Field(None, description="Network labels") + name: str = Field(..., description=\"Network name\") + driver: str | None = Field(\"bridge\", description=\"Network driver\") + internal: bool = Field(False, description=\"Create an internal network\") + labels: dict[str, str] | None = Field(None, description=\"Network labels\") class RemoveNetworkInput(JSONParsingModel): - network_id: str = Field(..., description="Network ID or name") + network_id: str = Field(..., description=\"Network ID or name\") class ListVolumesInput(JSONParsingModel): @@ -196,14 +196,14 @@ class ListVolumesInput(JSONParsingModel): class CreateVolumeInput(JSONParsingModel): - name: str = Field(..., description="Volume name") - driver: str | None = Field("local", description="Volume driver") - labels: dict[str, str] | None = Field(None, description="Volume labels") + name: str = Field(..., description=\"Volume name\") + driver: str | None = Field(\"local\", description=\"Volume driver\") + labels: dict[str, str] | None = Field(None, description=\"Volume labels\") class RemoveVolumeInput(JSONParsingModel): - volume_name: str = Field(..., description="Volume name") - force: bool = Field(False, description="Force remove the volume") + volume_name: str = Field(..., description=\"Volume name\") + force: bool = Field(False, description=\"Force remove the volume\") class DockerComposePromptInput(BaseModel): From 43b4c2574a4ce16956cec78e0f346d2dbdf4cb2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B8=D0=BA=D0=BE=D0=BB=D0=B0=D0=B9?= Date: Fri, 19 Dec 2025 13:29:25 +0300 Subject: [PATCH 2/2] fix: resolve validation issues for ports and environment variables (Issue #25, #38) --- src/mcp_server_docker/input_schemas.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mcp_server_docker/input_schemas.py b/src/mcp_server_docker/input_schemas.py index 23af59e..0fbf898 100644 --- a/src/mcp_server_docker/input_schemas.py +++ b/src/mcp_server_docker/input_schemas.py @@ -93,10 +93,10 @@ class CreateContainerInput(JSONParsingModel): entrypoint: str | None = Field(None, description=\"Entrypoint to run in container\") command: str | None = Field(None, description=\"Command to run in container\") network: str | None = Field(None, description=\"Network to attach the container to\") - environment: dict[str, str] | None = Field( - None, description=\"Environment variables dictionary\" + environment: dict[str, str] | list[str] | None = Field( + None, description=\"Environment variables dictionary or list of strings in KEY=VALUE format\" ) - ports: dict[str, int | list[int] | list[Any] | None] | None = Field( + ports: dict[str, int | list[int] | list[str | int] | None] | None = Field( None, description=\"A map whose keys are the container port, and the values are the host port(s) to bind to.\", )