[project] name = "docker-mcp-server" version = "2.2.9" description = "MCP server for managing Docker resources via the Docker SDK for Python" readme = "README.md" license = "MIT" license-files = ["LICENSE"] requires-python = ">=3.14" authors = [{ name = "Gavin Lucas" }] keywords = ["docker", "mcp", "model-context-protocol", "containers", "compose", "swarm", "devops"] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.14", "Topic :: System :: Systems Administration", "Topic :: Software Development :: Libraries", "Environment :: Console", "Operating System :: OS Independent", ] dependencies = [ # [ssh] pulls paramiko so `ssh://` DOCKER_HOST works through the pure-Python transport - no # system `ssh` binary, so it works identically on the host and inside the container images. # Floor is 7.2.0 because system.py:_build_default_client passes `use_context=False` to # `docker.from_env()`, a kwarg 7.2.0 introduced: 7.2.0 also made from_env resolve the active # Docker CLI context itself, which would duplicate (and could contradict) the context resolution # _hosts.py already does and pins at startup. On 7.1.0 that kwarg is not popped and reaches # kwargs_from_env as an unexpected argument, so this is a hard floor, not a preference - # tests/test_pyproject_pins.py::test_the_declared_docker_floor_supports_the_kwarg_the_code_passes # fails the day an installed docker-py stops accepting it. "docker[ssh]>=7.2.0", "httpx>=0.28.1", # server.py imports MCPServer from mcp.server.mcpserver (mcp 2.0.0 removed the 1.x # mcp.server.fastmcp.FastMCP surface this used to import). Floor 2.1.0: from that release the # SDK reports any exception other than ToolError/ResourceError as a generic crash, and the # registrars translate DockerMcpError into those so a refusal keeps its wording. On 2.0.0 the # translation is not merely unnecessary but wrong - it flattened even a deliberate # ResourceError - so a resource would explain nothing. No upper cap: there's no known 3.x # incompatibility to guard against, and the two guards in # tests/test_pyproject_pins.py fail CI the day an installed mcp stops providing the import path # or the error classification the code relies on. "mcp>=2.1.0", # Pulled in transitively by docker, but we import requests.exceptions directly # (system_wait's container wait catches ReadTimeout), so declare it explicitly. "requests>=2.26.0", # Also transitive (through requests), but imported directly: a read timeout partway through a # response body reaches the caller as requests' generic ConnectionError, and the container wait # matches urllib3's ReadTimeoutError as its cause to tell that apart from a real connection failure. "urllib3>=1.21.1", # Also transitive (through mcp), but imported directly: nodes.py configures a TypedDict with # `pydantic.with_config`. The floor is mcp's own, which has it. "pydantic>=2.12.0", ] [project.scripts] docker-mcp = "docker_mcp:main" docker-mcp-server = "docker_mcp:main" [project.urls] Homepage = "https://github.com/L337-org/docker-mcp" Repository = "https://github.com/L337-org/docker-mcp" Issues = "https://github.com/L337-org/docker-mcp/issues" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] only-include = ["docker_mcp"] [tool.uv] # Security pin: urllib3 < 2.7.0 has multiple CVEs. Pulled in transitively # via docker -> requests. Bump when a newer fix is available. constraint-dependencies = ["urllib3>=2.7.0"] [tool.ruff] line-length = 120 [tool.ruff.lint] # E/F = pycodestyle + pyflakes (ruff defaults), B = flake8-bugbear, S = flake8-bandit # (security; gives the S603/S310 noqas in _cli.py / resources.py teeth), UP = pyupgrade. select = ["E", "F", "B", "S", "UP", "D"] # No docstring rule is ignored blanket-wide: the whole of pydocstyle's Google convention applies, # and the two things it does not reach - tool docstrings and tests - are exempted by name below, # through ignore-decorators and per-file-ignores respectively. ignore = [] [tool.ruff.lint.pydocstyle] # CS.6.12: Google is the docstring format for Python, enforced here rather than by review. convention = "google" # Tool docstrings are NOT covered by these rules. A tool docstring is the description a client # loads and a model reads, so CS.6.14 hands it to the AI-consumer rules instead. # # That is not an argument against an `Args:` block - most tools have one, and a parameter's # purpose is worth a sentence wherever it is read. It is an argument against repeating each # parameter's *type*, which the `inputSchema` sent alongside already carries, and which would # then be paid for on every session that loads the surface. So a tool documents what its # arguments mean and leaves their types to the schema. # # Prompts and resources are deliberately absent, for two different reasons. A prompt takes its # description from `@prompt(description=...)`, so its docstring reaches no client. A resource's # docstring *is* advertised - `resources.py` passes no `description=` at all, and # `list_resources()` serves the docstring - but a resource advertises no parameter schema, so # there is no duplication to avoid. That is the whole basis of the exemption, so it does not # apply. Their docstrings still cost wire bytes: see tests/test_surface_budget.py. # # Fully-qualified paths through the module the decorator is imported FROM, which is what ruff # matches - not the bare name the call site uses. tests/test_server.py asserts this list against # the decorators actually in use, so a rename cannot silently pull the advertised descriptions # into rules that are wrong for them, and neither dead entry can come back. ignore-decorators = [ "docker_mcp.server.tool", ] [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401", "F403"] # Tests legitimately use assert (S101), hardcoded fixture secrets (S105/S106), # and /tmp paths as plain string fixtures, not real file operations (S108). "tests/**" = ["S101", "S105", "S106", "S108", "D"] # pydoclint checks the docstring against the signature; ruff's D417 only checks a section that # already exists, so a function documenting no parameters passes it. Run over `docker_mcp` only: # `tests/` is exempt as it is from D, and `scripts/` is outside the run too. # # Two exemptions are per function, both asserted by tests/test_docstrings.py - a tool's docstring # is its advertised description with the types already in `inputSchema` (CS.6.14), and # DOC501/DOC502/DOC503 on an advertised docstring are a decision rather than a backlog: it # carries no `Raises:` section because that is wire cost. Do not "clear" them. # # native-mode-noqa-location is load-bearing: pydoclint reads DOC noqa from the *closing docstring # line* by default, so without it every marker is ignored and the run still passes. [tool.pydoclint] style = "google" # Defaults to true, which would skip exactly the sectionless docstrings this is here to catch. skip-checking-short-docstrings = false # Fully annotated here, so types belong in the signature - the opposite of send-to-influx. arg-type-hints-in-signature = true arg-type-hints-in-docstring = false check-return-types = false check-yield-types = false allow-init-docstring = true native-mode-noqa-location = "definition" [tool.pyright] include = ["docker_mcp", "tests"] pythonVersion = "3.14" typeCheckingMode = "basic" reportMissingTypeStubs = false [tool.pytest.ini_options] markers = [ "integration: tests that require a real Docker daemon (run with `uv run pytest -m integration`)", ] addopts = "-m 'not integration'" [dependency-groups] dev = [ # Pulled in transitively by pytest, but tests/test_pyproject_pins.py parses PEP 508 requirement # strings with it directly, so declare it - the same reasoning as `requests` above. "packaging>=24.0", "pytest>=9.0.3", "pyright>=1.1.409", "ruff>=0.15.12", "pydoclint>=0.9.1", "pre-commit>=4.6.0", ]