mcps-docker-compose/MCP-STACK.md

91 lines
7.0 KiB
Markdown

# Secure MCP stack
The Docker Compose deployment of the MCP servers: the compose files, the gateway's Caddyfiles, the
egress proxy's configuration and the example `.env`. The servers themselves live in the `mcps`
repository, which builds their images and publishes them to Docker Hub; this repository only pulls
them, so a server running it needs neither the sources nor a build toolchain.
| Image | What it is |
|---|---|
| `luciolelii/coding-agent-mcp` | Scoped workspace editing. Task execution is disabled in this stack. |
| `luciolelii/dev-server-mcp` | Authenticated facade for a private, allowlisted development-server worker. One instance per execution, each in a throwaway copy of that execution's tree and on its own port. The same image runs the facade and the worker. |
| `luciolelii/browser-mcp` | Playwright automation restricted to exact allowed origins. |
| `luciolelii/minio-mcp` | Bounded list/read/stat/write access to object storage, with no delete tool. In internal mode (catalog entry `minio-mcp-internal`, header `x-minio-scope`) it uses the platform's MinIO whose endpoint and keys the catalog entry sends as headers, and one bucket per execution, `<MINIO_MCP_INTERNAL_BUCKET_PREFIX><execution id>`, created when the session opens; in external mode it connects to the MinIO endpoint and bucket of the node's configuration, which must exist, with the keys the model gives. |
| `luciolelii/postgres-mcp` | SQL access to PostgreSQL: `postgres_query` (read-only, bounded rows), `postgres_execute`, `postgres_list_tables` and `postgres_describe_table`. In internal mode (catalog entry `postgres-mcp-internal`, header `x-postgres-scope`) each execution gets a database and a user of its own, `<POSTGRES_MCP_INTERNAL_DATABASE_PREFIX><execution id>`, created when the session opens, which nobody else can connect to; the model may do anything inside it and nothing outside. In external mode it connects to the host, port and database of the node's configuration, which must exist, with the user and password the model gives. |
`egress-proxy` is the development worker's only route to package registries: it allows their CONNECT
requests and refuses everything else.
The MCP client orchestrates the servers; they do not share MCP sessions or credentials.
## Start
1. Copy `mcp-stack.env.example` to `.env` and replace every token with an independent random value. Every variable, required or optional, is documented in [ENVIRONMENT.md](ENVIRONMENT.md).
2. Set `MCP_WORKSPACE_HOST_PATH` to one dedicated project directory, owned by `10001:10001`
(`sudo chown -R 10001:10001 <dir>`): that is the user the images run as.
3. Copy and edit `dev-server-mcp/services.example.json`, and point `DEV_SERVER_SERVICES_CONFIG` at the copy. It ships four worked service definitions: a shared Vite tree, and per-execution ones for Node, Java (through the project's own `./mvnw`) and Python.
Keep `BROWSER_MCP_ALLOWED_ORIGINS` aligned with `DEV_SERVER_PORT_RANGE`: the browser reaches a preview only on a port that range covers.
4. Optionally set `MINIO_MCP_ALLOWED_ENDPOINTS` and `POSTGRES_MCP_ALLOWED_HOSTS` to the MinIO origins and PostgreSQL hosts external sessions may use. Without them, only public addresses are reachable.
5. If the Docker Hub repositories are private, run `docker login` on this host first.
6. Pull and run:
```sh
docker compose --env-file .env -f mcp-stack.compose.yml pull
docker compose --env-file .env -f mcp-stack.compose.yml up -d
```
On a dedicated VM add the overlay to **both** commands, or the gateway loses ports 80 and 443:
`-f mcp-stack.compose.yml -f mcp-stack.vm.compose.yml`.
## Updating
A deploy is a new image tag. Images are published from `mcps` and tagged with its commit hash and
`latest`. Set `MCP_IMAGE_TAG` in `.env` (or edit the default in `mcp-stack.compose.yml`) to the new
hash, then `pull` and `up -d` as above. To restart only some services without touching the others,
name them and add `--no-deps`: `up -d --no-deps minio-mcp mcp-gateway`.
Local MCP endpoints:
- `http://127.0.0.1:3101/mcp` — coding agent
- `http://127.0.0.1:3102/mcp` — development server
- `http://127.0.0.1:3103/mcp` — browser
- `http://127.0.0.1:3104/mcp` — MinIO
The same endpoints are also available through the unified gateway at `/coding-agent/mcp`,
`/dev-server/mcp`, `/browser/mcp` and `/minio/mcp` on port 3100 (or the configured HTTPS host on
the VM deployment).
Example Codex client configuration (`.codex/config.toml` in a trusted project):
```toml
[mcp_servers.coding_agent]
url = "http://127.0.0.1:3101/mcp"
bearer_token_env_var = "CODING_AGENT_MCP_TOKEN"
[mcp_servers.dev_server]
url = "http://127.0.0.1:3102/mcp"
bearer_token_env_var = "DEV_SERVER_MCP_TOKEN"
[mcp_servers.browser]
url = "http://127.0.0.1:3103/mcp"
bearer_token_env_var = "BROWSER_MCP_TOKEN"
[mcp_servers.minio]
url = "http://127.0.0.1:3104/mcp"
bearer_token_env_var = "MINIO_MCP_TOKEN"
[mcp_servers.postgres]
url = "http://127.0.0.1:3105/mcp"
bearer_token_env_var = "POSTGRES_MCP_TOKEN"
```
Set those variables in the MCP client's environment to the token portions configured for the corresponding servers. Do not reuse a token between servers.
Only the minimal Caddy gateway publishes loopback ports. The coding, development and browser services stay exclusively on internal networks. `minio-mcp` and `postgres-mcp` have outbound connectivity, because the internal mode reaches the platform's store and the external mode the one a flow author put in the node's configuration. The model never chooses where to connect. Without `MINIO_MCP_ALLOWED_ENDPOINTS` or `POSTGRES_MCP_ALLOWED_HOSTS` an external session reaches public addresses only, so a flow cannot point either server at something inside its own network; with them set, exactly the listed destinations. For remote use, use the included VM overlay so Caddy terminates TLS, and add rate limiting at the network edge when appropriate.
The worker and browser networks are marked internal, no service uses host networking or the Docker socket, and the development-server workspace mount is read-only. Container isolation reduces the blast radius but is not a substitute for an ephemeral VM/microVM when executing hostile multi-tenant code.
Installing dependencies means fetching and running other people's code, so three things stand between it and the rest of the stack: the tree runs in a copy of itself and never in the workspace mount, the install runs with package scripts disabled, and the only way out of the worker is a proxy that allows the registries and nothing else. Each execution also gets a `HOME` of its own, which is what keeps Maven's `~/.m2` from becoming a channel between executions — npm's shared cache is safe because `npm ci` verifies every package against the lockfile, and Maven has no lockfile to verify against.
`coding-agent-mcp` intentionally retains write access to the selected project because editing is its purpose. Point `MCP_WORKSPACE_HOST_PATH` only at a dedicated, backed-up directory on storage with a disk quota; never point it at a home directory, repository collection, or filesystem root.