docker2k8s-mcp
# docker2k8s-mcp
An MCP server that lets an LLM migrate a Docker / Docker Compose application to
Kubernetes — inspect it, plan the migration, generate manifests, validate them,
deploy them after you approve, verify the result, and diagnose what went wrong.
The interesting part is not the YAML. It is the loop:
```text
LLM ─▶ picks a tool ─▶ gets information ─▶ reasons ─▶ picks the next tool
─▶ generates configuration ─▶ validates ─▶ observes the deployment
─▶ diagnoses failures ─▶ iterates
```
**Works with any MCP client.** The server holds no agent logic and calls no LLM.
Use it from Claude Desktop, Claude Code, Cursor, VS Code, your own script, or the
reference agent included here.
---
## Contents
- [Architecture](#architecture)
- [Features](#features)
- [Installation](#installation)
- [Environment variables](#environment-variables)
- [Running the MCP server](#running-the-mcp-server)
- [Connecting a client](#connecting-a-client)
- [Running the reference client](#running-the-reference-client)
- [Requirements](#requirements)
- [Example migration](#example-migration)
- [Available MCP tools](#available-mcp-tools)
- [Security considerations](#security-considerations)
- [Development](#development)
- [Testing](#testing)
- [Roadmap](#roadmap)
---
## Architecture
```text
┌──────────────────────────────────────────────────────────┐
│ MCP Client + LLM │
│ Claude Desktop / Claude Code / Cursor / bundled agent │
└───────────────────────────┬──────────────────────────────┘
│ MCP (stdio | streamable HTTP | SSE)
▼
┌──────────────────────────────────────────────────────────┐
│ MCP Server docker2k8s_mcp/server.py │
└───────────────────────────┬──────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ Tools (thin wrappers) docker2k8s_mcp/tools/ │
└───────────────────────────┬──────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ Services docker2k8s_mcp/services/ │
│ docker_service ─▶ Filesystem + YAML │
│ migration_service ─▶ pure logic │
│ k8s_service ─▶ Kubernetes API + kubectl │
│ Generators / Validators │
└──────────────────────────────────────────────────────────┘
```
The migration pipeline:
```text
inspect ─▶ analyze ─▶ plan ─▶ generate ─▶ validate ─▶ APPROVAL ─▶ deploy
│
verify ◀────────────────┘
│
healthy? ─┴─ no ─▶ diagnose ─▶ iterate
```
Full detail: [docs/architecture.md](docs/architecture.md).
---
## Features
- **Understands a project, rather than transliterating YAML.** Parses Compose's
short and long syntaxes, Dockerfiles (multi-stage, line continuations, exec and
shell forms), and `.env` files.
- **Knows what does not translate.** Bind mounts, `depends_on` ordering, locally
built images, published database ports, privileged containers — each becomes a
warning with a suggested action, not a silent omission.
- **Chooses the right workload.** Datastores and services with named volumes
become StatefulSets with `volumeClaimTemplates` and a headless Service.
Everything else becomes a Deployment.
- **Real probes from `HEALTHCHECK`.** A `curl` healthcheck against localhost
becomes an `httpGet` probe; anything else becomes an `exec` probe. Readiness and
liveness get different timings, because they mean different things.
- **Never emits a credential.** Secret values are redacted at the parsing
boundary; generated Secrets contain `<REQUIRED_SECRET>` placeholders.
- **Validation that catches the silent failures.** Selectors that match nothing,
a `targetPort` no container listens on, probes on the wrong port, references to
a ConfigMap that does not exist.
- **Approval gate.** Deployment is refused without explicit approval, and refused
outright if validation failed.
- **Diagnosis.** CrashLoopBackOff, ImagePullBackOff, Pending, OOMKilled,
CreateContainerConfigError, failing probes, Services without endpoints — each
with evidence, likely cause and a suggested fix.
---
## Installation
Requires Python 3.12+.
```bash
git clone <your-repo> docker2k8s-mcp
cd docker2k8s-mcp
python -m venv .venv
# Windows
.venv\Scripts\activate
# macOS / Linux
source .venv/bin/activate
pip install -e .
# The reference LLM client and the test suite are optional extras:
pip install -e ".[client,dev]"
cp .env.example .env
```
---
## Environment variables
Everything is configured through the environment or `.env`.
| Variable | Default | Purpose |
|---|---|---|
| `OPENAI_API_KEY` | *(empty)* | Reference client only. The server never calls an LLM. |
| `OPENAI_MODEL` | `gpt-4o` | Model used by the reference client. |
| `OPENAI_BASE_URL` | *(unset)* | For OpenAI-compatible endpoints. |
| `KUBECTL_PATH` | `kubectl` | Path to the kubectl binary. |
| `KUBE_CONTEXT` | *(current)* | kubectl context to use. |
| `KUBE_NAMESPACE` | `default` | Default target namespace. |
| `GENERATED_DIR` | `./generated` | Where manifests are written. Relative to the checkout. |
| `ALLOWED_ROOTS` | checkout + cwd | **Filesystem sandbox.** OS-path-separator delimited. The working directory is only included when it is not a filesystem root or your home folder. |
| `LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR` or `CRITICAL`. |
| `DEBUG` | `false` | Include tracebacks in tool errors. |
| `KUBECTL_TIMEOUT` | `120` | Seconds before a kubectl call is aborted. |
| `MCP_AUTH_TOKEN` | *(empty)* | Bearer token for the http/sse transports. **Required** to listen on anything but loopback. |
`.env` is read from the checkout, not the working directory, so a server started
by an MCP client picks up the same settings. Never commit `.env`; it is git-ignored.
---
## Running the MCP server
```bash
# stdio — the default. Clients launch this themselves; you rarely run it by hand.
docker2k8s-mcp
# streamable HTTP, for remote or containerised use
docker2k8s-mcp --transport http --host 127.0.0.1 --port 8000 # -> http://127.0.0.1:8000/mcp
# SSE, for older HTTP clients
docker2k8s-mcp --transport sse --port 8000 # -> http://127.0.0.1:8000/sse
```
Or without installing: `python -m docker2k8s_mcp.main --transport http`.
Every tool, including `apply_manifests`, is reachable by whoever can connect, so
the HTTP transports **refuse to listen on a non-loopback address** unless
`MCP_AUTH_TOKEN` is set. Clients then send `Authorization: Bearer <token>`; the
bundled agent and web client read the token from `.env`. `--insecure-no-auth`
overrides the check for servers behind an authenticating proxy.
Logs go to stderr, because stdout carries the MCP protocol on the stdio transport.
### Running the MCP server in Docker
```bash
docker build -t docker2k8s-mcp .
docker run --rm -p 127.0.0.1:8000:8000 \
-e MCP_AUTH_TOKEN="$(openssl rand -hex 32)" \
-v "$HOME/.kube:/home/mcp/.kube:ro" \
-v "$PWD/examples:/workspace:ro" \
-v d2k-generated:/data \
docker2k8s-mcp
```
The container listens on `0.0.0.0` inside, so it will not start without
`MCP_AUTH_TOKEN`; publishing on `127.0.0.1` keeps it off your network as well.
Give clients the same token.
The server needs **kubectl and a kubeconfig**, not a Docker daemon — project
inspection is pure filesystem and YAML work, so no Docker-in-Docker is required.
Two caveats when containerising:
- A kubeconfig pointing at `127.0.0.1` (Docker Desktop, kind, minikube) will not
resolve from inside a container. Either rewrite the server URL to
`host.docker.internal`, or run the server on the host.
- Projects must be mounted into the container, and `ALLOWED_ROOTS` must include
the mount point (the image defaults it to `/workspace`).
**For local development, running the server directly on the host is simpler** and
is the recommended path.
---
## Connecting a client
The server is a standard MCP server. Point any client at it.
**Claude Desktop** — `claude_desktop_config.json`:
```json
{
"mcpServers": {
"docker2k8s": {
"command": "/absolute/path/to/docker2k8s-mcp/.venv/bin/docker2k8s-mcp",
"env": {
"ALLOWED_ROOTS": "/absolute/path/to/your/projects",
"KUBE_NAMESPACE": "default"
}
}
}
}
```
On Windows use `...\\.venv\\Scripts\\docker2k8s-mcp.exe` and `;` between roots.
**Claude Code**:
```bash
claude mcp add docker2k8s -- /absolute/path/to/.venv/bin/docker2k8s-mcp
```
**Cursor / VS Code / Windsurf** — same shape as Claude Desktop (`command`, `args`,
`env`), in that editor's MCP settings file.
**Any client, over HTTP** — start the server with `--transport http` and point the
client at `http://127.0.0.1:8000/mcp`, sending `Authorization: Bearer <token>` if
`MCP_AUTH_TOKEN` is set.
**From Python:**
```python
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
params = StdioServerParameters(command="docker2k8s-mcp")
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool("inspect_project", {"path": "examples/fastapi-mysql"})
print(result.structured_content)
```
The server sends its workflow as MCP `instructions` on initialize, and exposes
`migrate` and `diagnose` prompts, so a client needs no docker2k8s-specific code.
---
## Running the reference client
A minimal OpenAI-driven agent, included to demonstrate the full loop:
```bash
export OPENAI_API_KEY=sk-...
docker2k8s-agent "Migrate examples/fastapi-mysql to Kubernetes."
docker2k8s-agent # interactive
docker2k8s-agent --http http://127.0.0.1:8000/mcp "..." # sends MCP_AUTH_TOKEN if set
```
(`python -m docker2k8s_mcp.client.agent` works too.)
The model picks every tool. The client adds one thing: it prompts you at the
terminal before any call that could deploy, on top of the server's own approval
gate. Only a literal `dry_run: true` skips the prompt, and after you say yes the
client sends exactly the approved, non-dry-run call.
`python web_client.py` (or `run_web.bat`) serves the same agent as a chat page on
`http://localhost:3000`. It only accepts WebSocket connections from that page's
own origin, so other sites you have open cannot drive it.
---
## Requirements
### Docker
Only needed to *build and run* the example app, and to build images the cluster
will pull. The MCP server itself does not talk to the Docker daemon.
### Kubernetes
Target a local cluster first. Docker Desktop is the assumed default.
1. Docker Desktop → **Settings → Kubernetes → Enable Kubernetes** → Apply & restart.
2. Verify:
```bash
kubectl cluster-info
kubectl get nodes
```
You should see a control plane URL and at least one `Ready` node. If
`kubectl cluster-info` fails, every deployment tool will fail too — check this
first. `get_cluster_info` reports the same thing through MCP.
kind and minikube also work; set `KUBE_CONTEXT` accordingly.
---
## Example migration
[`examples/fastapi-mysql/`](examples/fastapi-mysql/) is a FastAPI service with a
MySQL database: a `/health` endpoint, a database connection, environment
variables, Compose networking, a persistent volume, healthchecks, an init-script
bind mount, and two replicas. It is deliberately chosen to show why this
migration is not a mechanical translation.
Ask any connected client:
> Migrate examples/fastapi-mysql to Kubernetes.
The agent inspects, analyses, plans, generates and validates, then shows you
something like:
```text
Resources: Deployment/api Service/api ConfigMap/api-config Secret/api-secret
StatefulSet/mysql Service/mysql ConfigMap/mysql-config Secret/mysql-secret
Ports: api: NodePort 8000 -> container 8000
mysql: ClusterIP 3306 -> container 3306
Secrets: DB_PASSWORD, MYSQL_PASSWORD, MYSQL_ROOT_PASSWORD
Volumes: mysql: mysql-data -> /var/lib/mysql (1Gi)
Health: api: httpGet readiness on /health
mysql: exec readiness
Warnings:
SECRETS_DETECTED Generated Secrets contain placeholders, never real values.
BIND_MOUNT './initdb' has no Kubernetes equivalent.
DEPENDS_ON Kubernetes does not order pod startup; the app must retry.
LOCAL_BUILD The cluster cannot build images.
Manual steps:
- docker build -t fastapi-mysql-api:local examples/fastapi-mysql
- kubectl port-forward svc/api 8000:8000
- Decide how to provide './initdb'
```
…then asks whether to deploy. Before saying yes:
```bash
# 1. Build the image so the cluster can find it (Docker Desktop shares its store)
docker build -t fastapi-mysql-api:local examples/fastapi-mysql
# 2. Create the Secrets (or fill the placeholders in generated/.../*-secret.yaml)
kubectl create secret generic mysql-secret \
--from-literal=MYSQL_ROOT_PASSWORD='...' \
--from-literal=MYSQL_PASSWORD='...' \
--dry-run=client -o yaml | kubectl apply -f -
```
Then approve. The agent applies the manifests, verifies the rollout, and
diagnoses anything that fails. `apply_manifests` leaves a Secret that already
exists alone instead of overwriting it with placeholders, and refuses to create
one that still holds them. Regenerating keeps values you typed into a generated
Secret file.
```bash
kubectl port-forward svc/api 8000:8000
curl http://localhost:8000/health
```
---
## Available MCP tools
### Inspection (read-only)
| Tool | Purpose |
|---|---|
| `inspect_project` | Dockerfiles, Compose file, `.env`, source dirs, service names. Start here. |
| `inspect_dockerfile` | Stages, base images, `EXPOSE`, `WORKDIR`, `USER`, `ENTRYPOINT`/`CMD`, `HEALTHCHECK`. |
| `inspect_compose` | Normalised services: ports, environment, volumes, `depends_on`, healthchecks, resources. |
| `inspect_environment` | Environment variables classified as config or secret. Values of secrets are never returned. |
### Analysis and planning (read-only)
| Tool | Purpose |
|---|---|
| `analyze_project` | How each Docker concept maps to Kubernetes; warnings and blockers. |
| `create_migration_plan` | The reviewable plan: workloads, ports, config/secrets, volumes, probes, manual steps. |
### Generation and validation
| Tool | Purpose |
|---|---|
| `generate_manifests` | Writes YAML to `generated/<project>/k8s`. Writes files only; earlier files the plan no longer produces become `*.stale`. |
| `validate_manifests` | Selector, port, port-name, probe, reference, duplicate, naming and label checks. |
### Cluster (read-only, except where noted)
| Tool | Purpose |
|---|---|
| `get_cluster_info` | Is a cluster reachable? Nodes and namespaces. |
| `apply_manifests` | **Destructive.** Requires `approved=true`; refuses invalid manifests, unsupported kinds, placeholder Secrets and objects owned by another project. `dry_run=true` is safe. |
| `get_deployment_status` | Desired vs ready replicas and rollout conditions. |
| `get_pods` | Phase, readiness, restarts, detected problems. |
| `get_pod_logs` | Container logs; `previous=true` for a crash loop. |
| `get_services` | Type, ports, selector, endpoint count. |
| `get_events` | Recent events, including warnings. |
| `verify_deployment` | All post-deployment checks in one call; optional `health_path` probe through the API proxy. |
| `diagnose_deployment` | Problem, evidence, likely cause, suggested fix. |
---
## Security considerations
**No arbitrary execution.** There is no `execute_shell` or `execute_kubectl`
tool. Every cluster capability is a separate named tool with a typed schema.
kubectl is invoked with an explicit argv list, never through a shell, and only
from `k8s_service.py`. Names are validated against DNS-1123 before they reach a
command line, and the kube context is passed as a single `--context=` argument,
so a crafted value cannot become a flag. `apply_manifests` deploys only
namespaced application kinds (plus a Namespace): nothing that grants RBAC or
node access. `health_path` is checked so it cannot leave the service proxy.
**Filesystem sandbox.** Paths are resolved (collapsing `..`) and then checked for
containment inside `ALLOWED_ROOTS`, which defaults to the checkout and the working
directory (never a filesystem root or your home folder). Anything outside is
refused, for reads and for `generate_manifests`' `output_dir` alike. Set
`ALLOWED_ROOTS` explicitly when running the server for someone else.
**Secrets never leave the boundary.** Values matching credential patterns — and
connection strings with embedded passwords — are replaced with `<REDACTED>` when
parsed, so they never reach the model, the logs or a manifest. Generated Secrets
contain `<REQUIRED_SECRET>` placeholders, and validation warns while they remain.
Fill them in with `kubectl create secret` or a secrets manager. A value that
references a secret `.env` variable is routed to the Secret, never resolved into
a ConfigMap, image name or command.
Kubernetes Secrets are base64-encoded, not encrypted. For production, enable
encryption at rest or use an external secrets operator.
**Deployment requires approval.** `apply_manifests` refuses without
`approved=true`, and refuses manifests that fail validation. It validates and
applies the same parsed documents, so nothing can change in between. The tool is
annotated `destructive_hint`, so clients that surface annotations will prompt as
well.
**The HTTP transports require a token off-loopback.** See *Running the MCP server*.
**Errors do not leak internals.** Domain errors return a message and a hint;
anything unexpected returns a generic failure and logs the traceback server-side.
Set `DEBUG=true` to include tracebacks in tool output while developing.
---
## Development
```text
docker2k8s_mcp/
main.py entry point, transports
http_auth.py bearer-token check for http/sse
server.py MCP server, tool registration, instructions
config.py settings
schemas.py Pydantic models
security.py path sandbox, secret detection
errors.py domain errors
tools/ thin MCP wrappers
services/ business logic
generators/ plan -> YAML
validators/ YAML -> issues
client/ reference LLM agent and shared client helpers
web_client.py browser chat client (with index.html)
examples/ example applications
tests/ unit + integration tests
```
Rules of the codebase:
- Tools stay thin; logic lives in services.
- All Kubernetes access goes through `k8s_service.py`.
- All path access goes through `resolve_project_path`.
- Never log or return a secret value.
- Use the standard `logging` module, not `print`.
---
## Testing
```bash
pip install -e ".[dev]"
pytest # unit tests; no Docker or Kubernetes needed
pytest -m integration # requires a running cluster
pytest --cov=docker2k8s_mcp # with coverage
```
Unit tests build small Docker projects in a `tmp_path` fixture, so they are fast
and hermetic. The cluster-side logic (apply, verify, diagnose) is tested against
a fake API and a fake kubectl. Integration tests are marked and excluded by default.
---
## Roadmap
Not implemented; documented as future work.
| Version | Scope |
|---|---|
| **V2** | Nginx configuration → Kubernetes Ingress migration |
| **V3** | AWS EKS support |
| **V4** | AWS infrastructure inspection |
| **V5** | Production readiness checker (probes, limits, PDBs, security contexts) |
| **V6** | Automatic remediation, with explicit approval |
The local Docker → Kubernetes pipeline is the priority; AWS work starts only once
it is solid end to end.
---
## License
MIT
TDQS
Scored across 17 tools
Each tool targets a distinct phase or resource: inspection, planning, generation, validation, deployment, and specific Kubernetes status checks. The descriptions clearly differentiate overlapping-looking tools like inspect_project, inspect_dockerfile, and inspect_compose by specifying their exact scope.
All tool names follow a consistent snake_case verb_noun pattern, with verbs like get, inspect, create, generate, validate, apply, verify, and diagnose. This makes the toolset highly predictable and easy for an agent to navigate.
17 tools is slightly above the typical 3-15 range, but the count is justified by the full migration-and-operations lifecycle the server covers. Each tool appears to serve a necessary, non-overlapping purpose, so the set feels slightly heavy yet reasonable.
The toolset covers the complete Docker-to-Kubernetes workflow from artifact inspection, analysis, planning, manifest generation, validation, deployment, and post-deployment verification/diagnosis. Minor gaps such as no explicit rollback or cleanup tool are present, but the core migration lifecycle has no dead ends.