Skip to content

feat: configurable context & container info #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ A powerful Model Context Protocol (MCP) server for Docker operations, enabling s
- 📦 Docker Compose stack deployment
- 🔍 Container logs retrieval
- 📊 Container listing and status monitoring
- 🌐 **Remote Docker host support** (via `DOCKER_HOST` and `DOCKER_CONTEXT`)
- 🔎 **Detailed container inspection** (ports, volumes, environment variables)

### 🎬 Demos
#### Deploying a Docker Compose Stack
Expand Down Expand Up @@ -46,6 +48,40 @@ To try this in Claude Desktop app, add this to your claude config files:
}
```

### Remote Docker Host Support

To connect to a remote Docker host, use environment variables:

```json
{
"mcpServers": {
"docker-mcp": {
"command": "uvx",
"args": ["docker-mcp"],
"env": {
"DOCKER_HOST": "ssh://user@remote-host"
}
}
}
}
```

Or use Docker contexts:

```json
{
"mcpServers": {
"docker-mcp": {
"command": "uvx",
"args": ["docker-mcp"],
"env": {
"DOCKER_CONTEXT": "remote-context"
}
}
}
}
```

### Installing via Smithery

To install Docker MCP for Claude Desktop automatically via [Smithery](https://smithery.ai/protocol/docker-mcp):
Expand Down Expand Up @@ -176,6 +212,23 @@ Lists all Docker containers
{}
```

### get-container-info
Get detailed information about a specific container including ports, volumes, environment variables, and resource limits
```json
{
"container_name": "my-container"
}
```

Returns comprehensive container details including:
- Container metadata (ID, status, image, creation time)
- Environment variables
- Port mappings
- Volume mounts
- Network settings
- Resource limits
- Working directory and command

## 🚧 Current Limitations

- No built-in environment variable support for containers
Expand Down
116 changes: 115 additions & 1 deletion src/docker_mcp/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,20 @@
from python_on_whales import DockerClient
from mcp.types import TextContent, Tool, Prompt, PromptArgument, GetPromptResult, PromptMessage
from .docker_executor import DockerComposeExecutor
docker_client = DockerClient()

def get_docker_client():
"""Get DockerClient with support for DOCKER_HOST and DOCKER_CONTEXT env vars."""
docker_host = os.getenv('DOCKER_HOST')
docker_context = os.getenv('DOCKER_CONTEXT')

if docker_host:
return DockerClient(host=docker_host)
elif docker_context:
return DockerClient(context_name=docker_context)
else:
return DockerClient()

docker_client = get_docker_client()


async def parse_port_mapping(host_key: str, container_port: str | int) -> tuple[str, str] | tuple[str, str, str]:
Expand Down Expand Up @@ -192,3 +205,104 @@ async def handle_list_containers(arguments: Dict[str, Any]) -> List[TextContent]
except Exception as e:
debug_output = "\n".join(debug_info)
return [TextContent(type="text", text=f"Error listing containers: {str(e)}\n\nDebug Information:\n{debug_output}")]

@staticmethod
async def handle_get_container_info(arguments: Dict[str, Any]) -> List[TextContent]:
debug_info = []
try:
container_name = arguments.get("container_name")
if not container_name:
raise ValueError("Missing required container_name")

debug_info.append(f"Getting detailed info for container '{container_name}'")

# Get full container inspection data
container = await asyncio.to_thread(docker_client.container.inspect, container_name)

# Format comprehensive container information
info_lines = [
f"=== Container Information ===",
f"Name: {container.name}",
f"ID: {container.id}",
f"Status: {container.state.status}",
f"Image: {container.config.image}",
f"Created: {container.created}",
f"Started: {container.state.started_at if hasattr(container.state, 'started_at') else 'N/A'}",
"",
f"=== Environment Variables ==="
]

# Environment variables
if container.config.env:
for env_var in container.config.env:
info_lines.append(f" {env_var}")
else:
info_lines.append(" No environment variables set")

info_lines.extend(["", f"=== Port Mappings ==="])

# Port mappings
if hasattr(container, 'network_settings') and container.network_settings.ports:
for container_port, host_configs in container.network_settings.ports.items():
if host_configs:
for host_config in host_configs:
info_lines.append(f" {host_config.get('HostIp', '0.0.0.0')}:{host_config['HostPort']} -> {container_port}")
else:
info_lines.append(f" {container_port} (not bound to host)")
else:
info_lines.append(" No port mappings")

info_lines.extend(["", f"=== Volume Mounts ==="])

# Volume mounts
if container.mounts:
for mount in container.mounts:
mount_type = getattr(mount, 'type', 'unknown')
source = getattr(mount, 'source', 'N/A')
destination = getattr(mount, 'destination', 'N/A')
mode = getattr(mount, 'mode', 'N/A')
info_lines.append(f" {mount_type}: {source} -> {destination} ({mode})")
else:
info_lines.append(" No volume mounts")

info_lines.extend(["", f"=== Network Settings ==="])

# Network information
if hasattr(container, 'network_settings'):
networks = getattr(container.network_settings, 'networks', {})
if networks:
for network_name, network_info in networks.items():
ip_address = getattr(network_info, 'ip_address', 'N/A')
info_lines.append(f" Network: {network_name} (IP: {ip_address})")
else:
info_lines.append(" No network information available")

info_lines.extend(["", f"=== Resource Limits ==="])

# Resource limits
if hasattr(container.host_config, 'memory') and container.host_config.memory:
memory_limit = container.host_config.memory / (1024*1024) # Convert to MB
info_lines.append(f" Memory Limit: {memory_limit:.0f}MB")
else:
info_lines.append(" Memory Limit: Not set")

if hasattr(container.host_config, 'cpu_shares') and container.host_config.cpu_shares:
info_lines.append(f" CPU Shares: {container.host_config.cpu_shares}")
else:
info_lines.append(" CPU Shares: Not set")

info_lines.extend(["", f"=== Working Directory & Command ==="])
info_lines.append(f" Working Dir: {getattr(container.config, 'working_dir', 'N/A')}")

if hasattr(container.config, 'cmd') and container.config.cmd:
cmd_str = ' '.join(container.config.cmd) if isinstance(container.config.cmd, list) else str(container.config.cmd)
info_lines.append(f" Command: {cmd_str}")
else:
info_lines.append(" Command: N/A")

container_info = "\n".join(info_lines)
return [TextContent(type="text", text=f"{container_info}\n\nDebug Info:\n{chr(10).join(debug_info)}")]

except Exception as e:
debug_output = "\n".join(debug_info)
return [TextContent(type="text", text=f"Error getting container info: {str(e)}\n\nDebug Information:\n{debug_output}")]
13 changes: 13 additions & 0 deletions src/docker_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,17 @@ async def handle_list_tools() -> List[types.Tool]:
"type": "object",
"properties": {}
}
),
types.Tool(
name="get-container-info",
description="Get detailed information about a specific container (ports, volumes, environment variables, etc.)",
inputSchema={
"type": "object",
"properties": {
"container_name": {"type": "string"}
},
"required": ["container_name"]
}
)
]

Expand All @@ -161,6 +172,8 @@ async def handle_call_tool(name: str, arguments: Dict[str, Any] | None) -> List[
return await DockerHandlers.handle_get_logs(arguments)
elif name == "list-containers":
return await DockerHandlers.handle_list_containers(arguments)
elif name == "get-container-info":
return await DockerHandlers.handle_get_container_info(arguments)
else:
raise ValueError(f"Unknown tool: {name}")
except Exception as e:
Expand Down