├── .gitignore
├── .python-version
├── LICENSE
├── README.md
├── pyproject.toml
├── src
└── docker_mcp
│ ├── __init__.py
│ ├── docker_executor.py
│ ├── handlers.py
│ └── server.py
└── uv.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # Python-generated files
2 | __pycache__/
3 | *.py[oc]
4 | build/
5 | dist/
6 | wheels/
7 | *.egg-info
8 |
9 | # Virtual environments
10 | .venv
11 | conf
12 | work
13 | README_PRIVATE.md
14 | docker_compose_files
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
1 | 3.12
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) [year] [fullname]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🐳 docker-mcp
2 |
3 | [](https://www.python.org/downloads/release/python-3120/)
4 | [](https://opensource.org/licenses/MIT)
5 | [](https://github.com/psf/black)
6 | [](https://smithery.ai/protocol/docker-mcp)
7 |
8 | A powerful Model Context Protocol (MCP) server for Docker operations, enabling seamless container and compose stack management through Claude AI.
9 |
10 | ## ✨ Features
11 |
12 | - 🚀 Container creation and instantiation
13 | - 📦 Docker Compose stack deployment
14 | - 🔍 Container logs retrieval
15 | - 📊 Container listing and status monitoring
16 |
17 | ### 🎬 Demos
18 | #### Deploying a Docker Compose Stack
19 |
20 |
21 | https://github.com/user-attachments/assets/b5f6e40a-542b-4a39-ba12-7fdf803ee278
22 |
23 |
24 |
25 | #### Analyzing Container Logs
26 |
27 |
28 |
29 | https://github.com/user-attachments/assets/da386eea-2fab-4835-82ae-896de955d934
30 |
31 |
32 |
33 | ## 🚀 Quickstart
34 |
35 | To try this in Claude Desktop app, add this to your claude config files:
36 | ```json
37 | {
38 | "mcpServers": {
39 | "docker-mcp": {
40 | "command": "uvx",
41 | "args": [
42 | "docker-mcp"
43 | ]
44 | }
45 | }
46 | }
47 | ```
48 |
49 | ### Installing via Smithery
50 |
51 | To install Docker MCP for Claude Desktop automatically via [Smithery](https://smithery.ai/protocol/docker-mcp):
52 |
53 | ```bash
54 | npx @smithery/cli install docker-mcp --client claude
55 | ```
56 |
57 | ### Prerequisites
58 |
59 | - UV (package manager)
60 | - Python 3.12+
61 | - Docker Desktop or Docker Engine
62 | - Claude Desktop
63 |
64 | ### Installation
65 |
66 | #### Claude Desktop Configuration
67 |
68 | Add the server configuration to your Claude Desktop config file:
69 |
70 | **MacOS**: `~/Library/Application\ Support/Claude/claude_desktop_config.json`
71 | **Windows**: `%APPDATA%/Claude/claude_desktop_config.json`
72 |
73 |
74 | 💻 Development Configuration
75 |
76 | ```json
77 | {
78 | "mcpServers": {
79 | "docker-mcp": {
80 | "command": "uv",
81 | "args": [
82 | "--directory",
83 | "",
84 | "run",
85 | "docker-mcp"
86 | ]
87 | }
88 | }
89 | }
90 | ```
91 |
92 |
93 |
94 | 🚀 Production Configuration
95 |
96 | ```json
97 | {
98 | "mcpServers": {
99 | "docker-mcp": {
100 | "command": "uvx",
101 | "args": [
102 | "docker-mcp"
103 | ]
104 | }
105 | }
106 | }
107 | ```
108 |
109 |
110 | ## 🛠️ Development
111 |
112 | ### Local Setup
113 |
114 | 1. Clone the repository:
115 | ```bash
116 | git clone https://github.com/QuantGeekDev/docker-mcp.git
117 | cd docker-mcp
118 | ```
119 |
120 | 2. Create and activate a virtual environment:
121 | ```bash
122 | python -m venv venv
123 | source venv/bin/activate # On Windows: venv\Scripts\activate
124 | ```
125 |
126 | 3. Install dependencies:
127 | ```bash
128 | uv sync
129 | ```
130 |
131 | ### 🔍 Debugging
132 |
133 | Launch the MCP Inspector for debugging:
134 |
135 | ```bash
136 | npx @modelcontextprotocol/inspector uv --directory run docker-mcp
137 | ```
138 |
139 | The Inspector will provide a URL to access the debugging interface.
140 |
141 | ## 📝 Available Tools
142 |
143 | The server provides the following tools:
144 |
145 | ### create-container
146 | Creates a standalone Docker container
147 | ```json
148 | {
149 | "image": "image-name",
150 | "name": "container-name",
151 | "ports": {"80": "80"},
152 | "environment": {"ENV_VAR": "value"}
153 | }
154 | ```
155 |
156 | ### deploy-compose
157 | Deploys a Docker Compose stack
158 | ```json
159 | {
160 | "project_name": "example-stack",
161 | "compose_yaml": "version: '3.8'\nservices:\n service1:\n image: image1:latest\n ports:\n - '8080:80'"
162 | }
163 | ```
164 |
165 | ### get-logs
166 | Retrieves logs from a specific container
167 | ```json
168 | {
169 | "container_name": "my-container"
170 | }
171 | ```
172 |
173 | ### list-containers
174 | Lists all Docker containers
175 | ```json
176 | {}
177 | ```
178 |
179 | ## 🚧 Current Limitations
180 |
181 | - No built-in environment variable support for containers
182 | - No volume management
183 | - No network management
184 | - No container health checks
185 | - No container restart policies
186 | - No container resource limits
187 |
188 | ## 🤝 Contributing
189 |
190 | 1. Fork the repository from [docker-mcp](https://github.com/QuantGeekDev/docker-mcp)
191 | 2. Create your feature branch
192 | 3. Commit your changes
193 | 4. Push to the branch
194 | 5. Open a Pull Request
195 |
196 | ## 📜 License
197 |
198 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
199 |
200 | ## ✨ Authors
201 |
202 | - **Alex Andru** - *Initial work | Core contributor* - [@QuantGeekDev](https://github.com/QuantGeekDev)
203 | - **Ali Sadykov** - *Initial work | Core contributor* - [@md-archive](https://github.com/md-archive)
204 |
205 | ---
206 | Made with ❤️
207 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "docker-mcp"
3 | version = "0.1.0"
4 | description = "A docker MCP server"
5 | readme = "README.md"
6 | requires-python = ">=3.12"
7 | dependencies = [
8 | "httpx>=0.28.0",
9 | "mcp>=1.0.0",
10 | "python-dotenv>=1.0.1",
11 | "python-on-whales>=0.67.0",
12 | "pyyaml>=6.0.1"
13 | ]
14 |
15 | [[project.authors]]
16 | name = "Alex Andru"
17 | email = "alex007d@gmail.com"
18 |
19 | [build-system]
20 | requires = ["hatchling"]
21 | build-backend = "hatchling.build"
22 |
23 | [project.scripts]
24 | docker-mcp = "docker_mcp:main"
--------------------------------------------------------------------------------
/src/docker_mcp/__init__.py:
--------------------------------------------------------------------------------
1 | from . import server
2 | import asyncio
3 |
4 | def main():
5 | """Main entry point for the package."""
6 | asyncio.run(server.main())
7 |
8 | # Optionally expose other important items at package level
9 | __all__ = ['main', 'server']
--------------------------------------------------------------------------------
/src/docker_mcp/docker_executor.py:
--------------------------------------------------------------------------------
1 | from typing import Tuple, Protocol, List
2 | import asyncio
3 | import os
4 | import platform
5 | import shutil
6 | from abc import ABC, abstractmethod
7 |
8 |
9 | class CommandExecutor(Protocol):
10 | async def execute(self, cmd: str | List[str]) -> Tuple[int, str, str]:
11 | pass
12 |
13 |
14 | class WindowsExecutor:
15 | async def execute(self, cmd: str) -> Tuple[int, str, str]:
16 | process = await asyncio.create_subprocess_shell(
17 | cmd,
18 | stdout=asyncio.subprocess.PIPE,
19 | stderr=asyncio.subprocess.PIPE,
20 | shell=True
21 | )
22 | stdout, stderr = await process.communicate()
23 | return process.returncode, stdout.decode(), stderr.decode()
24 |
25 |
26 | class UnixExecutor:
27 | async def execute(self, cmd: List[str]) -> Tuple[int, str, str]:
28 | process = await asyncio.create_subprocess_exec(
29 | *cmd,
30 | stdout=asyncio.subprocess.PIPE,
31 | stderr=asyncio.subprocess.PIPE
32 | )
33 | stdout, stderr = await process.communicate()
34 | return process.returncode, stdout.decode(), stderr.decode()
35 |
36 |
37 | class DockerExecutorBase(ABC):
38 | def __init__(self):
39 | self.docker_cmd = self._initialize_docker_cmd()
40 | self.executor = WindowsExecutor() if platform.system() == 'Windows' else UnixExecutor()
41 |
42 | @abstractmethod
43 | async def run_command(self, command: str, *args) -> Tuple[int, str, str]:
44 | pass
45 |
46 | def _initialize_docker_cmd(self) -> str:
47 | if platform.system() == 'Windows':
48 | docker_dir = r"C:\Program Files\Docker\Docker\resources\bin"
49 | docker_paths = [
50 | os.path.join(docker_dir, "docker-compose.exe"),
51 | os.path.join(docker_dir, "docker.exe")
52 | ]
53 | for path in docker_paths:
54 | if os.path.exists(path):
55 | return path
56 |
57 | docker_cmd = shutil.which('docker')
58 | if not docker_cmd:
59 | raise RuntimeError("Docker executable not found")
60 | return docker_cmd
61 |
62 |
63 | class DockerComposeExecutor(DockerExecutorBase):
64 | def __init__(self, compose_file: str, project_name: str):
65 | super().__init__()
66 | self.compose_file = os.path.abspath(compose_file)
67 | self.project_name = project_name
68 |
69 | async def run_command(self, command: str, *args) -> Tuple[int, str, str]:
70 | if platform.system() == 'Windows':
71 | cmd = self._build_windows_command(command, *args)
72 | else:
73 | cmd = self._build_unix_command(command, *args)
74 | return await self.executor.execute(cmd)
75 |
76 | def _build_windows_command(self, command: str, *args) -> str:
77 | compose_file = self.compose_file.replace('\\', '/')
78 | return (f'cd "{os.path.dirname(compose_file)}" && docker compose '
79 | f'-f "{os.path.basename(compose_file)}" '
80 | f'-p {self.project_name} {command} {" ".join(args)}')
81 |
82 | def _build_unix_command(self, command: str, *args) -> list[str]:
83 | return [
84 | self.docker_cmd,
85 | "compose",
86 | "-f", self.compose_file,
87 | "-p", self.project_name,
88 | command,
89 | *args
90 | ]
91 |
92 | async def down(self) -> Tuple[int, str, str]:
93 | return await self.run_command("down", "--volumes")
94 |
95 | async def pull(self) -> Tuple[int, str, str]:
96 | return await self.run_command("pull")
97 |
98 | async def up(self) -> Tuple[int, str, str]:
99 | return await self.run_command("up", "-d")
100 |
101 | async def ps(self) -> Tuple[int, str, str]:
102 | return await self.run_command("ps")
103 |
--------------------------------------------------------------------------------
/src/docker_mcp/handlers.py:
--------------------------------------------------------------------------------
1 | from typing import List, Dict, Any
2 | import asyncio
3 | import os
4 | import yaml
5 | import platform
6 | from python_on_whales import DockerClient
7 | from mcp.types import TextContent, Tool, Prompt, PromptArgument, GetPromptResult, PromptMessage
8 | from .docker_executor import DockerComposeExecutor
9 | docker_client = DockerClient()
10 |
11 |
12 | async def parse_port_mapping(host_key: str, container_port: str | int) -> tuple[str, str] | tuple[str, str, str]:
13 | if '/' in str(host_key):
14 | host_port, protocol = host_key.split('/')
15 | if protocol.lower() == 'udp':
16 | return (str(host_port), str(container_port), 'udp')
17 | return (str(host_port), str(container_port))
18 |
19 | if isinstance(container_port, str) and '/' in container_port:
20 | port, protocol = container_port.split('/')
21 | if protocol.lower() == 'udp':
22 | return (str(host_key), port, 'udp')
23 | return (str(host_key), port)
24 |
25 | return (str(host_key), str(container_port))
26 |
27 |
28 | class DockerHandlers:
29 | TIMEOUT_AMOUNT = 200
30 |
31 | @staticmethod
32 | async def handle_create_container(arguments: Dict[str, Any]) -> List[TextContent]:
33 | try:
34 | image = arguments["image"]
35 | container_name = arguments.get("name")
36 | ports = arguments.get("ports", {})
37 | environment = arguments.get("environment", {})
38 |
39 | if not image:
40 | raise ValueError("Image name cannot be empty")
41 |
42 | port_mappings = []
43 | for host_key, container_port in ports.items():
44 | mapping = await parse_port_mapping(host_key, container_port)
45 | port_mappings.append(mapping)
46 |
47 | async def pull_and_run():
48 | if not docker_client.image.exists(image):
49 | await asyncio.to_thread(docker_client.image.pull, image)
50 |
51 | container = await asyncio.to_thread(
52 | docker_client.container.run,
53 | image,
54 | name=container_name,
55 | publish=port_mappings,
56 | envs=environment,
57 | detach=True
58 | )
59 | return container
60 |
61 | container = await asyncio.wait_for(pull_and_run(), timeout=DockerHandlers.TIMEOUT_AMOUNT)
62 | return [TextContent(type="text", text=f"Created container '{container.name}' (ID: {container.id})")]
63 | except asyncio.TimeoutError:
64 | return [TextContent(type="text", text=f"Operation timed out after {DockerHandlers.TIMEOUT_AMOUNT} seconds")]
65 | except Exception as e:
66 | return [TextContent(type="text", text=f"Error creating container: {str(e)} | Arguments: {arguments}")]
67 |
68 | @staticmethod
69 | async def handle_deploy_compose(arguments: Dict[str, Any]) -> List[TextContent]:
70 | debug_info = []
71 | try:
72 | compose_yaml = arguments.get("compose_yaml")
73 | project_name = arguments.get("project_name")
74 |
75 | if not compose_yaml or not project_name:
76 | raise ValueError(
77 | "Missing required compose_yaml or project_name")
78 |
79 | yaml_content = DockerHandlers._process_yaml(
80 | compose_yaml, debug_info)
81 | compose_path = DockerHandlers._save_compose_file(
82 | yaml_content, project_name)
83 |
84 | try:
85 | result = await DockerHandlers._deploy_stack(compose_path, project_name, debug_info)
86 | return [TextContent(type="text", text=result)]
87 | finally:
88 | DockerHandlers._cleanup_files(compose_path)
89 |
90 | except Exception as e:
91 | debug_output = "\n".join(debug_info)
92 | return [TextContent(type="text", text=f"Error deploying compose stack: {str(e)}\n\nDebug Information:\n{debug_output}")]
93 |
94 | @staticmethod
95 | def _process_yaml(compose_yaml: str, debug_info: List[str]) -> dict:
96 | debug_info.append("=== Original YAML ===")
97 | debug_info.append(compose_yaml)
98 |
99 | try:
100 | yaml_content = yaml.safe_load(compose_yaml)
101 | debug_info.append("\n=== Loaded YAML Structure ===")
102 | debug_info.append(str(yaml_content))
103 | return yaml_content
104 | except yaml.YAMLError as e:
105 | raise ValueError(f"Invalid YAML format: {str(e)}")
106 |
107 | @staticmethod
108 | def _save_compose_file(yaml_content: dict, project_name: str) -> str:
109 | compose_dir = os.path.join(os.getcwd(), "docker_compose_files")
110 | os.makedirs(compose_dir, exist_ok=True)
111 |
112 | compose_yaml = yaml.safe_dump(
113 | yaml_content, default_flow_style=False, sort_keys=False)
114 | compose_path = os.path.join(
115 | compose_dir, f"{project_name}-docker-compose.yml")
116 |
117 | with open(compose_path, 'w', encoding='utf-8') as f:
118 | f.write(compose_yaml)
119 | f.flush()
120 | if platform.system() != 'Windows':
121 | os.fsync(f.fileno())
122 |
123 | return compose_path
124 |
125 | @staticmethod
126 | async def _deploy_stack(compose_path: str, project_name: str, debug_info: List[str]) -> str:
127 | compose = DockerComposeExecutor(compose_path, project_name)
128 |
129 | for command in [compose.down, compose.up]:
130 | try:
131 | code, out, err = await command()
132 | debug_info.extend([
133 | f"\n=== {command.__name__.capitalize()} Command ===",
134 | f"Return Code: {code}",
135 | f"Stdout: {out}",
136 | f"Stderr: {err}"
137 | ])
138 |
139 | if code != 0 and command == compose.up:
140 | raise Exception(f"Deploy failed with code {code}: {err}")
141 | except Exception as e:
142 | if command != compose.down:
143 | raise e
144 | debug_info.append(f"Warning during {
145 | command.__name__}: {str(e)}")
146 |
147 | code, out, err = await compose.ps()
148 | service_info = out if code == 0 else "Unable to list services"
149 |
150 | return (f"Successfully deployed compose stack '{project_name}'\n"
151 | f"Running services:\n{service_info}\n\n"
152 | f"Debug Info:\n{chr(10).join(debug_info)}")
153 |
154 | @staticmethod
155 | def _cleanup_files(compose_path: str) -> None:
156 | try:
157 | if os.path.exists(compose_path):
158 | os.remove(compose_path)
159 | compose_dir = os.path.dirname(compose_path)
160 | if os.path.exists(compose_dir) and not os.listdir(compose_dir):
161 | os.rmdir(compose_dir)
162 | except Exception as e:
163 | print(f"Warning during cleanup: {str(e)}")
164 |
165 | @staticmethod
166 | async def handle_get_logs(arguments: Dict[str, Any]) -> List[TextContent]:
167 | debug_info = []
168 | try:
169 | container_name = arguments.get("container_name")
170 | if not container_name:
171 | raise ValueError("Missing required container_name")
172 |
173 | debug_info.append(f"Fetching logs for container '{
174 | container_name}'")
175 | logs = await asyncio.to_thread(docker_client.container.logs, container_name, tail=100)
176 |
177 | return [TextContent(type="text", text=f"Logs for container '{container_name}':\n{logs}\n\nDebug Info:\n{chr(10).join(debug_info)}")]
178 | except Exception as e:
179 | debug_output = "\n".join(debug_info)
180 | return [TextContent(type="text", text=f"Error retrieving logs: {str(e)}\n\nDebug Information:\n{debug_output}")]
181 |
182 | @staticmethod
183 | async def handle_list_containers(arguments: Dict[str, Any]) -> List[TextContent]:
184 | debug_info = []
185 | try:
186 | debug_info.append("Listing all Docker containers")
187 | containers = await asyncio.to_thread(docker_client.container.list, all=True)
188 | container_list = "\n".join(
189 | [f"{c.id[:12]} - {c.name} - {c.state.status}" for c in containers])
190 |
191 | return [TextContent(type="text", text=f"All Docker Containers:\n{container_list}\n\nDebug Info:\n{chr(10).join(debug_info)}")]
192 | except Exception as e:
193 | debug_output = "\n".join(debug_info)
194 | return [TextContent(type="text", text=f"Error listing containers: {str(e)}\n\nDebug Information:\n{debug_output}")]
195 |
--------------------------------------------------------------------------------
/src/docker_mcp/server.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import signal
3 | import sys
4 | from typing import List, Dict, Any
5 | import mcp.types as types
6 | from mcp.server import NotificationOptions, Server
7 | from mcp.server.models import InitializationOptions
8 | import mcp.server.stdio
9 | from .handlers import DockerHandlers
10 |
11 | server = Server("docker-mcp")
12 |
13 |
14 | @server.list_prompts()
15 | async def handle_list_prompts() -> List[types.Prompt]:
16 | return [
17 | types.Prompt(
18 | name="deploy-stack",
19 | description="Generate and deploy a Docker stack based on requirements",
20 | arguments=[
21 | types.PromptArgument(
22 | name="requirements",
23 | description="Description of the desired Docker stack",
24 | required=True
25 | ),
26 | types.PromptArgument(
27 | name="project_name",
28 | description="Name for the Docker Compose project",
29 | required=True
30 | )
31 | ]
32 | )
33 | ]
34 |
35 |
36 | @server.get_prompt()
37 | async def handle_get_prompt(name: str, arguments: Dict[str, str] | None) -> types.GetPromptResult:
38 | if name != "deploy-stack":
39 | raise ValueError(f"Unknown prompt: {name}")
40 |
41 | if not arguments or "requirements" not in arguments or "project_name" not in arguments:
42 | raise ValueError("Missing required arguments")
43 |
44 | system_message = (
45 | "You are a Docker deployment specialist. Generate appropriate Docker Compose YAML or "
46 | "container configurations based on user requirements. For simple single-container "
47 | "deployments, use the create-container tool. For multi-container deployments, generate "
48 | "a docker-compose.yml and use the deploy-compose tool. To access logs, first use the "
49 | "list-containers tool to discover running containers, then use the get-logs tool to "
50 | "retrieve logs for a specific container."
51 | )
52 |
53 | user_message = f"""Please help me deploy the following stack:
54 | Requirements: {arguments['requirements']}
55 | Project name: {arguments['project_name']}
56 |
57 | Analyze if this needs a single container or multiple containers. Then:
58 | 1. For single container: Use the create-container tool with format:
59 | {{
60 | "image": "image-name",
61 | "name": "container-name",
62 | "ports": {{"80": "80"}},
63 | "environment": {{"ENV_VAR": "value"}}
64 | }}
65 |
66 | 2. For multiple containers: Use the deploy-compose tool with format:
67 | {{
68 | "project_name": "example-stack",
69 | "compose_yaml": "version: '3.8'\\nservices:\\n service1:\\n image: image1:latest\\n ports:\\n - '8080:80'"
70 | }}"""
71 |
72 | return types.GetPromptResult(
73 | description="Generate and deploy a Docker stack",
74 | messages=[
75 | types.PromptMessage(
76 | role="system",
77 | content=types.TextContent(
78 | type="text",
79 | text=system_message
80 | )
81 | ),
82 | types.PromptMessage(
83 | role="user",
84 | content=types.TextContent(
85 | type="text",
86 | text=user_message
87 | )
88 | )
89 | ]
90 | )
91 |
92 |
93 | @server.list_tools()
94 | async def handle_list_tools() -> List[types.Tool]:
95 | return [
96 | types.Tool(
97 | name="create-container",
98 | description="Create a new standalone Docker container",
99 | inputSchema={
100 | "type": "object",
101 | "properties": {
102 | "image": {"type": "string"},
103 | "name": {"type": "string"},
104 | "ports": {
105 | "type": "object",
106 | "additionalProperties": {"type": "string"}
107 | },
108 | "environment": {
109 | "type": "object",
110 | "additionalProperties": {"type": "string"}
111 | }
112 | },
113 | "required": ["image"]
114 | }
115 | ),
116 | types.Tool(
117 | name="deploy-compose",
118 | description="Deploy a Docker Compose stack",
119 | inputSchema={
120 | "type": "object",
121 | "properties": {
122 | "compose_yaml": {"type": "string"},
123 | "project_name": {"type": "string"}
124 | },
125 | "required": ["compose_yaml", "project_name"]
126 | }
127 | ),
128 | types.Tool(
129 | name="get-logs",
130 | description="Retrieve the latest logs for a specified Docker container",
131 | inputSchema={
132 | "type": "object",
133 | "properties": {
134 | "container_name": {"type": "string"}
135 | },
136 | "required": ["container_name"]
137 | }
138 | ),
139 | types.Tool(
140 | name="list-containers",
141 | description="List all Docker containers",
142 | inputSchema={
143 | "type": "object",
144 | "properties": {}
145 | }
146 | )
147 | ]
148 |
149 |
150 | @server.call_tool()
151 | async def handle_call_tool(name: str, arguments: Dict[str, Any] | None) -> List[types.TextContent]:
152 | if not arguments and name != "list-containers":
153 | raise ValueError("Missing arguments")
154 |
155 | try:
156 | if name == "create-container":
157 | return await DockerHandlers.handle_create_container(arguments)
158 | elif name == "deploy-compose":
159 | return await DockerHandlers.handle_deploy_compose(arguments)
160 | elif name == "get-logs":
161 | return await DockerHandlers.handle_get_logs(arguments)
162 | elif name == "list-containers":
163 | return await DockerHandlers.handle_list_containers(arguments)
164 | else:
165 | raise ValueError(f"Unknown tool: {name}")
166 | except Exception as e:
167 | return [types.TextContent(type="text", text=f"Error: {str(e)} | Arguments: {arguments}")]
168 |
169 |
170 | async def main():
171 | signal.signal(signal.SIGINT, handle_shutdown)
172 | signal.signal(signal.SIGTERM, handle_shutdown)
173 |
174 | async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
175 | await server.run(
176 | read_stream,
177 | write_stream,
178 | InitializationOptions(
179 | server_name="docker-mcp",
180 | server_version="0.1.0",
181 | capabilities=server.get_capabilities(
182 | notification_options=NotificationOptions(),
183 | experimental_capabilities={},
184 | ),
185 | ),
186 | )
187 |
188 |
189 | def handle_shutdown(signum, frame):
190 | print("Shutting down gracefully...")
191 | sys.exit(0)
192 |
193 |
194 | if __name__ == "__main__":
195 | asyncio.run(main())
196 |
--------------------------------------------------------------------------------
/uv.lock:
--------------------------------------------------------------------------------
1 | version = 1
2 | requires-python = ">=3.12"
3 |
4 | [[package]]
5 | name = "annotated-types"
6 | version = "0.7.0"
7 | source = { registry = "https://pypi.org/simple" }
8 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
9 | wheels = [
10 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
11 | ]
12 |
13 | [[package]]
14 | name = "anyio"
15 | version = "4.6.2.post1"
16 | source = { registry = "https://pypi.org/simple" }
17 | dependencies = [
18 | { name = "idna" },
19 | { name = "sniffio" },
20 | ]
21 | sdist = { url = "https://files.pythonhosted.org/packages/9f/09/45b9b7a6d4e45c6bcb5bf61d19e3ab87df68e0601fa8c5293de3542546cc/anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", size = 173422 }
22 | wheels = [
23 | { url = "https://files.pythonhosted.org/packages/e4/f5/f2b75d2fc6f1a260f340f0e7c6a060f4dd2961cc16884ed851b0d18da06a/anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d", size = 90377 },
24 | ]
25 |
26 | [[package]]
27 | name = "certifi"
28 | version = "2024.8.30"
29 | source = { registry = "https://pypi.org/simple" }
30 | sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 }
31 | wheels = [
32 | { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 },
33 | ]
34 |
35 | [[package]]
36 | name = "click"
37 | version = "8.1.7"
38 | source = { registry = "https://pypi.org/simple" }
39 | dependencies = [
40 | { name = "colorama", marker = "platform_system == 'Windows'" },
41 | ]
42 | sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 }
43 | wheels = [
44 | { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 },
45 | ]
46 |
47 | [[package]]
48 | name = "colorama"
49 | version = "0.4.6"
50 | source = { registry = "https://pypi.org/simple" }
51 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
52 | wheels = [
53 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
54 | ]
55 |
56 | [[package]]
57 | name = "docker-mcp"
58 | version = "0.1.0"
59 | source = { editable = "." }
60 | dependencies = [
61 | { name = "httpx" },
62 | { name = "mcp" },
63 | { name = "python-dotenv" },
64 | { name = "python-on-whales" },
65 | { name = "pyyaml" },
66 | ]
67 |
68 | [package.metadata]
69 | requires-dist = [
70 | { name = "httpx", specifier = ">=0.28.0" },
71 | { name = "mcp", specifier = ">=1.0.0" },
72 | { name = "python-dotenv", specifier = ">=1.0.1" },
73 | { name = "python-on-whales", specifier = ">=0.67.0" },
74 | { name = "pyyaml", specifier = ">=6.0.1" },
75 | ]
76 |
77 | [[package]]
78 | name = "h11"
79 | version = "0.14.0"
80 | source = { registry = "https://pypi.org/simple" }
81 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
82 | wheels = [
83 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
84 | ]
85 |
86 | [[package]]
87 | name = "httpcore"
88 | version = "1.0.7"
89 | source = { registry = "https://pypi.org/simple" }
90 | dependencies = [
91 | { name = "certifi" },
92 | { name = "h11" },
93 | ]
94 | sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 }
95 | wheels = [
96 | { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 },
97 | ]
98 |
99 | [[package]]
100 | name = "httpx"
101 | version = "0.28.0"
102 | source = { registry = "https://pypi.org/simple" }
103 | dependencies = [
104 | { name = "anyio" },
105 | { name = "certifi" },
106 | { name = "httpcore" },
107 | { name = "idna" },
108 | ]
109 | sdist = { url = "https://files.pythonhosted.org/packages/10/df/676b7cf674dd1bdc71a64ad393c89879f75e4a0ab8395165b498262ae106/httpx-0.28.0.tar.gz", hash = "sha256:0858d3bab51ba7e386637f22a61d8ccddaeec5f3fe4209da3a6168dbb91573e0", size = 141307 }
110 | wheels = [
111 | { url = "https://files.pythonhosted.org/packages/8f/fb/a19866137577ba60c6d8b69498dc36be479b13ba454f691348ddf428f185/httpx-0.28.0-py3-none-any.whl", hash = "sha256:dc0b419a0cfeb6e8b34e85167c0da2671206f5095f1baa9663d23bcfd6b535fc", size = 73551 },
112 | ]
113 |
114 | [[package]]
115 | name = "httpx-sse"
116 | version = "0.4.0"
117 | source = { registry = "https://pypi.org/simple" }
118 | sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 }
119 | wheels = [
120 | { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 },
121 | ]
122 |
123 | [[package]]
124 | name = "idna"
125 | version = "3.10"
126 | source = { registry = "https://pypi.org/simple" }
127 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
128 | wheels = [
129 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
130 | ]
131 |
132 | [[package]]
133 | name = "mcp"
134 | version = "1.0.0"
135 | source = { registry = "https://pypi.org/simple" }
136 | dependencies = [
137 | { name = "anyio" },
138 | { name = "httpx" },
139 | { name = "httpx-sse" },
140 | { name = "pydantic" },
141 | { name = "sse-starlette" },
142 | { name = "starlette" },
143 | ]
144 | sdist = { url = "https://files.pythonhosted.org/packages/97/de/a9ec0a1b6439f90ea59f89004bb2e7ec6890dfaeef809751d9e6577dca7e/mcp-1.0.0.tar.gz", hash = "sha256:dba51ce0b5c6a80e25576f606760c49a91ee90210fed805b530ca165d3bbc9b7", size = 82891 }
145 | wheels = [
146 | { url = "https://files.pythonhosted.org/packages/56/89/900c0c8445ec001d3725e475fc553b0feb2e8a51be018f3bb7de51e683db/mcp-1.0.0-py3-none-any.whl", hash = "sha256:bbe70ffa3341cd4da78b5eb504958355c68381fb29971471cea1e642a2af5b8a", size = 36361 },
147 | ]
148 |
149 | [[package]]
150 | name = "pydantic"
151 | version = "2.10.2"
152 | source = { registry = "https://pypi.org/simple" }
153 | dependencies = [
154 | { name = "annotated-types" },
155 | { name = "pydantic-core" },
156 | { name = "typing-extensions" },
157 | ]
158 | sdist = { url = "https://files.pythonhosted.org/packages/41/86/a03390cb12cf64e2a8df07c267f3eb8d5035e0f9a04bb20fb79403d2a00e/pydantic-2.10.2.tar.gz", hash = "sha256:2bc2d7f17232e0841cbba4641e65ba1eb6fafb3a08de3a091ff3ce14a197c4fa", size = 785401 }
159 | wheels = [
160 | { url = "https://files.pythonhosted.org/packages/d5/74/da832196702d0c56eb86b75bfa346db9238617e29b0b7ee3b8b4eccfe654/pydantic-2.10.2-py3-none-any.whl", hash = "sha256:cfb96e45951117c3024e6b67b25cdc33a3cb7b2fa62e239f7af1378358a1d99e", size = 456364 },
161 | ]
162 |
163 | [[package]]
164 | name = "pydantic-core"
165 | version = "2.27.1"
166 | source = { registry = "https://pypi.org/simple" }
167 | dependencies = [
168 | { name = "typing-extensions" },
169 | ]
170 | sdist = { url = "https://files.pythonhosted.org/packages/a6/9f/7de1f19b6aea45aeb441838782d68352e71bfa98ee6fa048d5041991b33e/pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235", size = 412785 }
171 | wheels = [
172 | { url = "https://files.pythonhosted.org/packages/be/51/2e9b3788feb2aebff2aa9dfbf060ec739b38c05c46847601134cc1fed2ea/pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f", size = 1895239 },
173 | { url = "https://files.pythonhosted.org/packages/7b/9e/f8063952e4a7d0127f5d1181addef9377505dcce3be224263b25c4f0bfd9/pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02", size = 1805070 },
174 | { url = "https://files.pythonhosted.org/packages/2c/9d/e1d6c4561d262b52e41b17a7ef8301e2ba80b61e32e94520271029feb5d8/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c", size = 1828096 },
175 | { url = "https://files.pythonhosted.org/packages/be/65/80ff46de4266560baa4332ae3181fffc4488ea7d37282da1a62d10ab89a4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac", size = 1857708 },
176 | { url = "https://files.pythonhosted.org/packages/d5/ca/3370074ad758b04d9562b12ecdb088597f4d9d13893a48a583fb47682cdf/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb", size = 2037751 },
177 | { url = "https://files.pythonhosted.org/packages/b1/e2/4ab72d93367194317b99d051947c071aef6e3eb95f7553eaa4208ecf9ba4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529", size = 2733863 },
178 | { url = "https://files.pythonhosted.org/packages/8a/c6/8ae0831bf77f356bb73127ce5a95fe115b10f820ea480abbd72d3cc7ccf3/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35", size = 2161161 },
179 | { url = "https://files.pythonhosted.org/packages/f1/f4/b2fe73241da2429400fc27ddeaa43e35562f96cf5b67499b2de52b528cad/pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089", size = 1993294 },
180 | { url = "https://files.pythonhosted.org/packages/77/29/4bb008823a7f4cc05828198153f9753b3bd4c104d93b8e0b1bfe4e187540/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381", size = 2001468 },
181 | { url = "https://files.pythonhosted.org/packages/f2/a9/0eaceeba41b9fad851a4107e0cf999a34ae8f0d0d1f829e2574f3d8897b0/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb", size = 2091413 },
182 | { url = "https://files.pythonhosted.org/packages/d8/36/eb8697729725bc610fd73940f0d860d791dc2ad557faaefcbb3edbd2b349/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae", size = 2154735 },
183 | { url = "https://files.pythonhosted.org/packages/52/e5/4f0fbd5c5995cc70d3afed1b5c754055bb67908f55b5cb8000f7112749bf/pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c", size = 1833633 },
184 | { url = "https://files.pythonhosted.org/packages/ee/f2/c61486eee27cae5ac781305658779b4a6b45f9cc9d02c90cb21b940e82cc/pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16", size = 1986973 },
185 | { url = "https://files.pythonhosted.org/packages/df/a6/e3f12ff25f250b02f7c51be89a294689d175ac76e1096c32bf278f29ca1e/pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e", size = 1883215 },
186 | { url = "https://files.pythonhosted.org/packages/0f/d6/91cb99a3c59d7b072bded9959fbeab0a9613d5a4935773c0801f1764c156/pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073", size = 1895033 },
187 | { url = "https://files.pythonhosted.org/packages/07/42/d35033f81a28b27dedcade9e967e8a40981a765795c9ebae2045bcef05d3/pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08", size = 1807542 },
188 | { url = "https://files.pythonhosted.org/packages/41/c2/491b59e222ec7e72236e512108ecad532c7f4391a14e971c963f624f7569/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf", size = 1827854 },
189 | { url = "https://files.pythonhosted.org/packages/e3/f3/363652651779113189cefdbbb619b7b07b7a67ebb6840325117cc8cc3460/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737", size = 1857389 },
190 | { url = "https://files.pythonhosted.org/packages/5f/97/be804aed6b479af5a945daec7538d8bf358d668bdadde4c7888a2506bdfb/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2", size = 2037934 },
191 | { url = "https://files.pythonhosted.org/packages/42/01/295f0bd4abf58902917e342ddfe5f76cf66ffabfc57c2e23c7681a1a1197/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107", size = 2735176 },
192 | { url = "https://files.pythonhosted.org/packages/9d/a0/cd8e9c940ead89cc37812a1a9f310fef59ba2f0b22b4e417d84ab09fa970/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51", size = 2160720 },
193 | { url = "https://files.pythonhosted.org/packages/73/ae/9d0980e286627e0aeca4c352a60bd760331622c12d576e5ea4441ac7e15e/pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a", size = 1992972 },
194 | { url = "https://files.pythonhosted.org/packages/bf/ba/ae4480bc0292d54b85cfb954e9d6bd226982949f8316338677d56541b85f/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc", size = 2001477 },
195 | { url = "https://files.pythonhosted.org/packages/55/b7/e26adf48c2f943092ce54ae14c3c08d0d221ad34ce80b18a50de8ed2cba8/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960", size = 2091186 },
196 | { url = "https://files.pythonhosted.org/packages/ba/cc/8491fff5b608b3862eb36e7d29d36a1af1c945463ca4c5040bf46cc73f40/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23", size = 2154429 },
197 | { url = "https://files.pythonhosted.org/packages/78/d8/c080592d80edd3441ab7f88f865f51dae94a157fc64283c680e9f32cf6da/pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05", size = 1833713 },
198 | { url = "https://files.pythonhosted.org/packages/83/84/5ab82a9ee2538ac95a66e51f6838d6aba6e0a03a42aa185ad2fe404a4e8f/pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337", size = 1987897 },
199 | { url = "https://files.pythonhosted.org/packages/df/c3/b15fb833926d91d982fde29c0624c9f225da743c7af801dace0d4e187e71/pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", size = 1882983 },
200 | ]
201 |
202 | [[package]]
203 | name = "python-dotenv"
204 | version = "1.0.1"
205 | source = { registry = "https://pypi.org/simple" }
206 | sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 }
207 | wheels = [
208 | { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
209 | ]
210 |
211 | [[package]]
212 | name = "python-on-whales"
213 | version = "0.74.0"
214 | source = { registry = "https://pypi.org/simple" }
215 | dependencies = [
216 | { name = "pydantic" },
217 | { name = "typing-extensions" },
218 | ]
219 | sdist = { url = "https://files.pythonhosted.org/packages/d3/70/324510b13ab03fcc666ac88789a211e05a2f363a3ea39a1d5110a420ede2/python_on_whales-0.74.0.tar.gz", hash = "sha256:edc21d4c28a952d779433e417f85719928aff53cef9c8a77e6fdc7eae0cf05fa", size = 110412 }
220 | wheels = [
221 | { url = "https://files.pythonhosted.org/packages/80/e3/a1b024a67f0194807ae3ca6783c57637126a1c768535ed10e54ffc5f72e6/python_on_whales-0.74.0-py3-none-any.whl", hash = "sha256:94d1d2f633bcf4bbc81d29ea76a0eadba1b88bb4d1bb4cfeffde2312e08135ca", size = 115589 },
222 | ]
223 |
224 | [[package]]
225 | name = "pyyaml"
226 | version = "6.0.2"
227 | source = { registry = "https://pypi.org/simple" }
228 | sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 }
229 | wheels = [
230 | { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 },
231 | { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 },
232 | { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 },
233 | { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 },
234 | { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 },
235 | { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 },
236 | { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 },
237 | { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 },
238 | { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 },
239 | { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 },
240 | { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 },
241 | { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 },
242 | { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 },
243 | { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 },
244 | { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 },
245 | { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 },
246 | { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 },
247 | { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
248 | ]
249 |
250 | [[package]]
251 | name = "sniffio"
252 | version = "1.3.1"
253 | source = { registry = "https://pypi.org/simple" }
254 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
255 | wheels = [
256 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
257 | ]
258 |
259 | [[package]]
260 | name = "sse-starlette"
261 | version = "2.1.3"
262 | source = { registry = "https://pypi.org/simple" }
263 | dependencies = [
264 | { name = "anyio" },
265 | { name = "starlette" },
266 | { name = "uvicorn" },
267 | ]
268 | sdist = { url = "https://files.pythonhosted.org/packages/72/fc/56ab9f116b2133521f532fce8d03194cf04dcac25f583cf3d839be4c0496/sse_starlette-2.1.3.tar.gz", hash = "sha256:9cd27eb35319e1414e3d2558ee7414487f9529ce3b3cf9b21434fd110e017169", size = 19678 }
269 | wheels = [
270 | { url = "https://files.pythonhosted.org/packages/52/aa/36b271bc4fa1d2796311ee7c7283a3a1c348bad426d37293609ca4300eef/sse_starlette-2.1.3-py3-none-any.whl", hash = "sha256:8ec846438b4665b9e8c560fcdea6bc8081a3abf7942faa95e5a744999d219772", size = 9383 },
271 | ]
272 |
273 | [[package]]
274 | name = "starlette"
275 | version = "0.41.3"
276 | source = { registry = "https://pypi.org/simple" }
277 | dependencies = [
278 | { name = "anyio" },
279 | ]
280 | sdist = { url = "https://files.pythonhosted.org/packages/1a/4c/9b5764bd22eec91c4039ef4c55334e9187085da2d8a2df7bd570869aae18/starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835", size = 2574159 }
281 | wheels = [
282 | { url = "https://files.pythonhosted.org/packages/96/00/2b325970b3060c7cecebab6d295afe763365822b1306a12eeab198f74323/starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7", size = 73225 },
283 | ]
284 |
285 | [[package]]
286 | name = "typing-extensions"
287 | version = "4.12.2"
288 | source = { registry = "https://pypi.org/simple" }
289 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
290 | wheels = [
291 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
292 | ]
293 |
294 | [[package]]
295 | name = "uvicorn"
296 | version = "0.32.1"
297 | source = { registry = "https://pypi.org/simple" }
298 | dependencies = [
299 | { name = "click" },
300 | { name = "h11" },
301 | ]
302 | sdist = { url = "https://files.pythonhosted.org/packages/6a/3c/21dba3e7d76138725ef307e3d7ddd29b763119b3aa459d02cc05fefcff75/uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175", size = 77630 }
303 | wheels = [
304 | { url = "https://files.pythonhosted.org/packages/50/c1/2d27b0a15826c2b71dcf6e2f5402181ef85acf439617bb2f1453125ce1f3/uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e", size = 63828 },
305 | ]
306 |
--------------------------------------------------------------------------------