Deploy postgres-mcp beside the other servers
A postgres-mcp service on its own internal control network and an egress one, a /postgres/* route in both Caddyfiles (and :3105 in the loopback one), and the variables, documented in ENVIRONMENT.md and the example environment. Its external mode is off until POSTGRES_MCP_ALLOWED_HOSTS lists the hosts it may use. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4f0d78b81d
commit
ea6e6ac443
|
|
@ -42,7 +42,7 @@ Generate a token with `openssl rand -hex 32`.
|
|||
|
||||
| Variable | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `MCP_IMAGE_TAG` | the hash named in `mcp-stack.compose.yml` | Which build of the four `luciolelii/*` images to run: a commit hash of the `mcps` repository, or `latest`. Bumping it is a deploy. |
|
||||
| `MCP_IMAGE_TAG` | the hash named in `mcp-stack.compose.yml` | Which build of the five `luciolelii/*` images to run: a commit hash of the `mcps` repository, or `latest`. Bumping it is a deploy. |
|
||||
|
||||
**MinIO MCP**
|
||||
|
||||
|
|
@ -59,6 +59,28 @@ The internal MinIO's endpoint and keys are **not** variables here: they are head
|
|||
`minio-mcp-internal` entry in the workflow manager's catalog (`x-minio-endpoint`,
|
||||
`x-minio-access-key`, `x-minio-secret-key`, optionally `x-minio-region`).
|
||||
|
||||
**PostgreSQL MCP**
|
||||
|
||||
| Variable | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `POSTGRES_MCP_API_KEYS` | none, **required** | Bearer tokens, as for the other servers; at least 32 characters each. |
|
||||
| `POSTGRES_MCP_INTERNAL_DATABASE_PREFIX` | `exec_` | Every internal-mode database, and its user, is `<prefix><execution id>` with the id's dashes as underscores. Cannot be empty; at most 26 lowercase letters, digits or `_`, starting with a letter. It is the boundary that keeps the API key inside databases this server made. |
|
||||
| `POSTGRES_MCP_ALLOWED_HOSTS` | empty | Comma-separated host names this server may connect to. **The external mode is off while it is empty**: the model could otherwise aim the server at any address it reaches. For the internal mode it is optional; when set, the platform's PostgreSQL host has to be on it. |
|
||||
| `POSTGRES_MCP_MAX_ROWS` | `1000` | Most rows one query returns, whatever the model asks. Ceiling 10000. |
|
||||
| `POSTGRES_MCP_MAX_RESULT_BYTES` | `1048576` (1 MiB) | Most bytes of rows one statement returns; the rest is cut and the answer says so. Ceiling 16 MiB. |
|
||||
| `POSTGRES_MCP_STATEMENT_TIMEOUT_MS` | `30000` | A statement running longer is stopped by the server. Ceiling 10 minutes. |
|
||||
| `POSTGRES_MCP_MAX_CONNECTIONS_PER_SESSION` | `4` | External mode only. Ceiling 32. |
|
||||
|
||||
The internal PostgreSQL's host and credentials are **not** variables here: they are headers of the
|
||||
`postgres-mcp-internal` entry in the workflow manager's catalog (`x-postgres-host`, `x-postgres-port`,
|
||||
`x-postgres-database`, `x-postgres-user`, `x-postgres-password`, `x-postgres-sslmode`, and
|
||||
`x-postgres-role-secret`). The user has to be able to make databases and users - `CREATEDB` and
|
||||
`CREATEROLE`, and nothing more - and the secret, at least 32 characters, is what the password of each
|
||||
execution's own user is derived from. **It has to be the same as the `executionRoleSecret` of the
|
||||
workflow manager's own `storages.json` entry for that PostgreSQL**, and so does the prefix, or the
|
||||
storage nodes and the models would not see the same database. The databases are dropped when they are
|
||||
older than the entry's `executionDatabaseExpireDays` by the workflow manager, not by this server.
|
||||
|
||||
**Dev server**
|
||||
|
||||
| Variable | Default | Meaning |
|
||||
|
|
@ -197,3 +219,20 @@ cannot be set through `.env`. Defaults shown are the ones in the code.
|
|||
| `MINIO_MCP_REQUEST_MAX_BYTES` | derived from `MAX_OBJECT_BYTES` | HTTP body limit; base64 adds a third. |
|
||||
| `MINIO_MCP_HOST` / `_PORT` | `0.0.0.0` / `3000` | |
|
||||
| `MINIO_MCP_CORS_ORIGINS` | empty | |
|
||||
|
||||
### PostgreSQL MCP
|
||||
|
||||
| Variable | Default | |
|
||||
|---|---|---|
|
||||
| `POSTGRES_MCP_API_KEYS` | none, **required** | |
|
||||
| `POSTGRES_MCP_INTERNAL_DATABASE_PREFIX` | `exec_` | See above. |
|
||||
| `POSTGRES_MCP_ALLOWED_HOSTS` | empty | See above: empty turns the external mode off. |
|
||||
| `POSTGRES_MCP_MAX_ROWS` | `1000` | Ceiling 10000. |
|
||||
| `POSTGRES_MCP_MAX_RESULT_BYTES` | `1048576` | Ceiling 16 MiB. |
|
||||
| `POSTGRES_MCP_STATEMENT_TIMEOUT_MS` | `30000` | |
|
||||
| `POSTGRES_MCP_MAX_CONNECTIONS_PER_SESSION` | `4` | Ceiling 32. |
|
||||
| `POSTGRES_MCP_MAX_SESSIONS` | `16` | |
|
||||
| `POSTGRES_MCP_SESSION_TTL_SECONDS` | `1800` | |
|
||||
| `POSTGRES_MCP_REQUEST_MAX_BYTES` | `1048576` | HTTP body limit. |
|
||||
| `POSTGRES_MCP_HOST` / `_PORT` | `0.0.0.0` / `3000` | |
|
||||
| `POSTGRES_MCP_CORS_ORIGINS` | empty | |
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ them, so a server running it needs neither the sources nor a build toolchain.
|
|||
| `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 the model opens session-scoped connections to MinIO deployments itself. |
|
||||
| `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 the model opens a connection to a host the server allows, and the mode is off until `POSTGRES_MCP_ALLOWED_HOSTS` names one. |
|
||||
|
||||
`egress-proxy` is the development worker's only route to package registries: it allows their CONNECT
|
||||
requests and refuses everything else.
|
||||
|
|
@ -72,11 +73,15 @@ 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 four variables in the MCP client's environment to the token portions configured for the corresponding servers. Do not reuse a token between servers.
|
||||
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` has outbound connectivity because each MCP session can open connections to remote S3 endpoints. Set `MINIO_MCP_ALLOWED_ENDPOINTS` whenever those endpoints are known; leaving it empty gives authenticated MCP clients an intentional network-request capability. For remote use, use the included VM overlay so Caddy terminates TLS, and add rate limiting at the network edge when appropriate.
|
||||
Only the minimal Caddy gateway publishes loopback ports. The coding, development and browser services stay exclusively on internal networks. `minio-mcp` has outbound connectivity because each MCP session can open connections to remote S3 endpoints. Set `MINIO_MCP_ALLOWED_ENDPOINTS` whenever those endpoints are known; leaving it empty gives authenticated MCP clients an intentional network-request capability. `postgres-mcp` is the same, and stricter by default: it has outbound connectivity because the internal mode reaches the platform's PostgreSQL and the external mode a host the model names, but the external mode is off until `POSTGRES_MCP_ALLOWED_HOSTS` lists the hosts it may use. 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
handle_path /postgres/* {
|
||||
reverse_proxy postgres-mcp:3000 {
|
||||
flush_interval -1
|
||||
}
|
||||
}
|
||||
|
||||
# The one door a person opens, as opposed to the MCP routes above, which a machine opens with an API
|
||||
# key. handle_path strips the prefix, so the worker's proxy sees /<execution-key>/... and the
|
||||
# application behind it sees the path it would see at the root.
|
||||
|
|
@ -72,3 +78,9 @@
|
|||
flush_interval -1
|
||||
}
|
||||
}
|
||||
|
||||
:3105 {
|
||||
reverse_proxy postgres-mcp:3000 {
|
||||
flush_interval -1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -222,6 +222,40 @@ services:
|
|||
restart: unless-stopped
|
||||
logging: *default-logging
|
||||
|
||||
postgres-mcp:
|
||||
image: luciolelii/postgres-mcp:${MCP_IMAGE_TAG:-921d54c}
|
||||
environment:
|
||||
POSTGRES_MCP_API_KEYS: ${POSTGRES_MCP_API_KEYS}
|
||||
POSTGRES_MCP_MAX_ROWS: ${POSTGRES_MCP_MAX_ROWS:-1000}
|
||||
POSTGRES_MCP_MAX_RESULT_BYTES: ${POSTGRES_MCP_MAX_RESULT_BYTES:-1048576}
|
||||
POSTGRES_MCP_STATEMENT_TIMEOUT_MS: ${POSTGRES_MCP_STATEMENT_TIMEOUT_MS:-30000}
|
||||
POSTGRES_MCP_MAX_CONNECTIONS_PER_SESSION: ${POSTGRES_MCP_MAX_CONNECTIONS_PER_SESSION:-4}
|
||||
# Comma-separated host names the server may connect to. For the external mode it is the whole of its
|
||||
# permission: empty, and the model cannot open a connection of its own. For the internal mode it is
|
||||
# optional, and when set the platform's PostgreSQL has to be on it.
|
||||
POSTGRES_MCP_ALLOWED_HOSTS: ${POSTGRES_MCP_ALLOWED_HOSTS:-}
|
||||
# Internal mode: the workflow manager's catalog entry postgres-mcp-internal sends the platform
|
||||
# PostgreSQL's host and the credentials of the user that makes databases (CREATEDB, CREATEROLE) with
|
||||
# x-postgres-scope, and the database and user <prefix><execution id> are created on first use. Only the
|
||||
# prefix is this server's. The host is reached over postgres-egress.
|
||||
POSTGRES_MCP_INTERNAL_DATABASE_PREFIX: ${POSTGRES_MCP_INTERNAL_DATABASE_PREFIX:-exec_}
|
||||
user: "${MCP_UID:-10001}:${MCP_GID:-10001}"
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp:rw,noexec,nosuid,size=16m
|
||||
cap_drop: [ALL]
|
||||
security_opt: [no-new-privileges:true]
|
||||
pids_limit: 64
|
||||
mem_limit: 256m
|
||||
cpus: 0.5
|
||||
init: true
|
||||
expose: ["3000"]
|
||||
# postgres-control is the private path from Caddy. postgres-egress is needed because each logical
|
||||
# session may point at a different PostgreSQL host, and the internal one is reached the same way.
|
||||
networks: [postgres-control, postgres-egress]
|
||||
restart: unless-stopped
|
||||
logging: *default-logging
|
||||
|
||||
mcp-gateway:
|
||||
image: caddy:2
|
||||
user: "${MCP_UID:-10001}:${MCP_GID:-10001}"
|
||||
|
|
@ -265,7 +299,8 @@ services:
|
|||
- "127.0.0.1:${DEV_SERVER_MCP_PORT:-3102}:3102"
|
||||
- "127.0.0.1:${BROWSER_MCP_PORT:-3103}:3103"
|
||||
- "127.0.0.1:${MINIO_MCP_PORT:-3104}:3104"
|
||||
networks: [gateway, coding-control, dev-server-control, workspace-browser, minio-control]
|
||||
- "127.0.0.1:${POSTGRES_MCP_PORT:-3105}:3105"
|
||||
networks: [gateway, coding-control, dev-server-control, workspace-browser, minio-control, postgres-control]
|
||||
depends_on:
|
||||
caddy-data-init:
|
||||
condition: service_completed_successfully
|
||||
|
|
@ -277,6 +312,8 @@ services:
|
|||
condition: service_started
|
||||
minio-mcp:
|
||||
condition: service_started
|
||||
postgres-mcp:
|
||||
condition: service_started
|
||||
restart: unless-stopped
|
||||
logging: *default-logging
|
||||
|
||||
|
|
@ -312,6 +349,9 @@ networks:
|
|||
minio-control:
|
||||
internal: true
|
||||
minio-egress:
|
||||
postgres-control:
|
||||
internal: true
|
||||
postgres-egress:
|
||||
# The worker and the proxy meet here, and nothing else does. Internal, so joining it grants no
|
||||
# route out: the proxy's own egress comes from the separate network below, which only it joins.
|
||||
proxy-control:
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ CODING_AGENT_MCP_API_KEYS=agent=replace-with-at-least-32-random-characters
|
|||
DEV_SERVER_MCP_API_KEYS=agent=replace-with-a-different-32-char-token
|
||||
BROWSER_MCP_API_KEYS=agent=replace-with-a-third-32-char-token
|
||||
MINIO_MCP_API_KEYS=agent=replace-with-a-fourth-32-char-token
|
||||
POSTGRES_MCP_API_KEYS=agent=replace-with-a-fifth-32-char-token
|
||||
DEV_SERVER_WORKER_TOKEN=replace-with-a-private-worker-token-32-chars
|
||||
|
||||
# External mode: URL, bucket and credentials are passed once to minio_open_session, not configured
|
||||
|
|
@ -30,6 +31,21 @@ MINIO_MCP_INTERNAL_BUCKET_PREFIX=exec-
|
|||
# bucket is created). 0 keeps them for ever. The empty bucket itself stays.
|
||||
MINIO_MCP_INTERNAL_BUCKET_EXPIRE_DAYS=7
|
||||
|
||||
# PostgreSQL MCP. External mode: host, database and credentials are passed once to postgres_open_session.
|
||||
# It is OFF while this list is empty - the model could otherwise aim the server at any address it can reach -
|
||||
# so name the hosts it may use, comma-separated. For the internal mode the list is optional.
|
||||
POSTGRES_MCP_ALLOWED_HOSTS=
|
||||
POSTGRES_MCP_MAX_ROWS=1000
|
||||
POSTGRES_MCP_MAX_RESULT_BYTES=1048576
|
||||
POSTGRES_MCP_STATEMENT_TIMEOUT_MS=30000
|
||||
#
|
||||
# The internal mode is chosen by the catalog entry postgres-mcp-internal, and its host and credentials are
|
||||
# that entry's headers in the workflow manager's catalog - not variables here. The session arrives with
|
||||
# x-postgres-scope set to the execution's id, and this server makes the database and the user
|
||||
# <prefix><execution id> on first use. The catalog's user needs CREATEDB and CREATEROLE, and the secret in
|
||||
# its headers has to be the same as the executionRoleSecret of the manager's own storages.json entry.
|
||||
POSTGRES_MCP_INTERNAL_DATABASE_PREFIX=exec_
|
||||
|
||||
# Use a dedicated project directory, never a home directory or filesystem root.
|
||||
MCP_WORKSPACE_HOST_PATH=/absolute/path/to/project
|
||||
# Whoever owns the workspace directory on the host. This file is read literally - no shell runs
|
||||
|
|
|
|||
|
|
@ -46,6 +46,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
handle_path /postgres/* {
|
||||
reverse_proxy postgres-mcp:3000 {
|
||||
flush_interval -1
|
||||
}
|
||||
}
|
||||
|
||||
# The one door a person opens, as opposed to the MCP routes above, which a machine opens with an API
|
||||
# key. handle_path strips the prefix, so the worker's proxy sees /<execution-key>/... and the
|
||||
# application behind it sees the path it would see at the root.
|
||||
|
|
|
|||
Loading…
Reference in New Issue