Skip to content
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
23 changes: 23 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
FROM python:3.12-alpine

WORKDIR /app

RUN apk add --no-cache docker-cli

RUN pip install --no-cache-dir uv

COPY pyproject.toml README.md ./
COPY src/ src/

RUN uv pip install --system --no-cache .

EXPOSE 8080

ENV DOCKER_MCP_TRANSPORT=http
ENV DOCKER_MCP_HOST=0.0.0.0
ENV DOCKER_MCP_PORT=8080

HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')"

ENTRYPOINT ["docker-mcp"]
27 changes: 27 additions & 0 deletions docker-compose.example.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
# docker-mcp — containerised HTTP deployment example
#
# Exposes the MCP server at http://localhost:4001/mcp
# Mount the Docker socket read-only so docker-mcp can manage containers
# without requiring a Portainer or remote Docker API.
#
# MCP client config (.mcp.json or claude_desktop_config.json):
# {
# "mcpServers": {
# "docker-mcp": { "type": "http", "url": "http://localhost:4001/mcp" }
# }
# }

services:
docker-mcp:
build: .
# or: image: ghcr.io/quantgeekdev/docker-mcp:latest (once published)
container_name: docker-mcp
restart: unless-stopped
ports:
- "127.0.0.1:4001:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
DOCKER_MCP_TRANSPORT: http
DOCKER_MCP_PORT: "8080"
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"httpx>=0.28.0",
"mcp>=1.0.0",
"mcp>=1.6.0",
"python-dotenv>=1.0.1",
"python-on-whales>=0.67.0",
"pyyaml>=6.0.1"
"pyyaml>=6.0.1",
"starlette>=0.40.0",
"uvicorn>=0.27.0",
]

[[project.authors]]
Expand Down
98 changes: 80 additions & 18 deletions src/docker_mcp/server.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import argparse
import asyncio
import os
import signal
import sys
from typing import List, Dict, Any
from contextlib import asynccontextmanager
from typing import Any, AsyncIterator, Dict, List

import mcp.server.stdio
import mcp.types as types
from mcp.server import NotificationOptions, Server
from mcp.server.models import InitializationOptions
import mcp.server.stdio

from .handlers import DockerHandlers

server = Server("docker-mcp")
Expand Down Expand Up @@ -167,23 +172,80 @@ async def handle_call_tool(name: str, arguments: Dict[str, Any] | None) -> List[
return [types.TextContent(type="text", text=f"Error: {str(e)} | Arguments: {arguments}")]


def _init_options() -> InitializationOptions:
return InitializationOptions(
server_name="docker-mcp",
server_version="0.1.0",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
),
)


def _create_starlette_app():
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import Mount, Route

session_manager = StreamableHTTPSessionManager(
app=server,
event_store=None,
json_response=False,
stateless=True,
)

@asynccontextmanager
async def lifespan(app: Starlette) -> AsyncIterator[None]:
async with session_manager.run():
yield

async def health(request: Request) -> JSONResponse:
return JSONResponse({"status": "ok"})

return Starlette(
lifespan=lifespan,
routes=[
Route("/health", endpoint=health),
Mount("/mcp", app=session_manager.handle_request),
],
)


async def main():
signal.signal(signal.SIGINT, handle_shutdown)
signal.signal(signal.SIGTERM, handle_shutdown)

async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
InitializationOptions(
server_name="docker-mcp",
server_version="0.1.0",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
),
),
)
parser = argparse.ArgumentParser(description="docker-mcp: Docker MCP server")
parser.add_argument(
"--transport",
choices=["stdio", "http"],
default=os.environ.get("DOCKER_MCP_TRANSPORT", "stdio"),
help="Transport mode: stdio (default) or http",
)
parser.add_argument(
"--port",
type=int,
default=int(os.environ.get("DOCKER_MCP_PORT", "8080")),
help="HTTP port (default: 8080, only used with --transport http)",
)
parser.add_argument(
"--host",
default=os.environ.get("DOCKER_MCP_HOST", "0.0.0.0"),
help="HTTP host (default: 0.0.0.0, only used with --transport http)",
)
args = parser.parse_args()

if args.transport == "http":
import uvicorn
app = _create_starlette_app()
config = uvicorn.Config(app, host=args.host, port=args.port, log_level="info")
srv = uvicorn.Server(config)
await srv.serve()
else:
signal.signal(signal.SIGINT, handle_shutdown)
signal.signal(signal.SIGTERM, handle_shutdown)
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream, _init_options())


def handle_shutdown(signum, frame):
Expand Down
Loading