afd0428823
* feat(serve): turnkey secure remote MCP server (#1877) Add `mempalace serve`: a secure-by-default wrapper over the HTTP MCP transport so a team can stand up a shared central palace with one command. Server capabilities (mempalace/mcp_server.py): - Native TLS via --tls-cert/--tls-key (env MEMPALACE_MCP_TLS_CERT/_KEY): wraps the socket in a TLS 1.2+ context, validated before bind. Token is still required on a non-loopback bind (TLS != auth). - Read-only mode via --read-only (env MEMPALACE_MCP_READ_ONLY): the 24 mutating tools are hidden from tools/list and refused at dispatch (-32003), enforced before arg handling — not merely hidden. Turnkey command (mempalace/cli.py): - Auto-generates a strong bearer token for non-loopback binds, stored 0600 under ~/.mempalace/server/ and printed once; reused across restarts. Token rides in the child env, never argv, so it can't leak via ps. - Prints a ready-to-paste client config (scheme reflects TLS), then foreground-execs the real server so Docker/systemd own the lifecycle. Deployment (deploy/): - docker-compose.server.yml wires the server + Qdrant with a /healthz healthcheck and persistent volumes. - server.env.example documents the env surface. - mempalace-server.service is a hardened systemd unit template. Tests: TLS handshake (openssl-gated), read-only enforcement, token autogen/0600/reuse, token-not-in-argv, secure-by-default gates. Docs: remote-server guide now leads with `mempalace serve` plus Compose and systemd subsections. * test(serve): fix Windows — don't patch os.name; gate 0600 asserts to POSIX Patching os.name to 'posix' broke Path.home() on Windows (pathlib mixed POSIX home resolution with Windows drive parsing). Capture both exec branches (os.execve + subprocess.run) instead, and guard the POSIX permission-bit assertions behind os.name == 'posix' (Windows files report 0o666).
72 lines
2.6 KiB
YAML
72 lines
2.6 KiB
YAML
# MemPalace remote team server — MCP over HTTP, backed by a central Qdrant.
|
|
#
|
|
# One command stands up a shared memory service a whole team's AI clients
|
|
# connect to. Embeddings are still produced locally inside the mempalace
|
|
# container; only your own Qdrant ever receives the vectors and text.
|
|
#
|
|
# 1. cp deploy/server.env.example deploy/.env && edit deploy/.env
|
|
# (at minimum set MEMPALACE_MCP_HTTP_TOKEN to a long random secret)
|
|
# 2. docker compose -f deploy/docker-compose.server.yml --env-file deploy/.env up -d
|
|
# 3. connect a client (see the Remote / Team Server guide):
|
|
# claude mcp add --transport http mempalace http://YOUR_HOST:8765/mcp \
|
|
# --header "Authorization: Bearer $MEMPALACE_MCP_HTTP_TOKEN"
|
|
#
|
|
# SECURITY: this exposes plaintext HTTP on :8765. For anything beyond a trusted
|
|
# private network, put a TLS-terminating reverse proxy in front (nginx/Caddy/
|
|
# Traefik) and only expose the proxy. The bearer token is mandatory for the
|
|
# network-exposed (0.0.0.0) bind.
|
|
|
|
services:
|
|
qdrant:
|
|
image: qdrant/qdrant:latest
|
|
restart: unless-stopped
|
|
volumes:
|
|
- qdrant-storage:/qdrant/storage
|
|
# Not published to the host: only the mempalace service reaches it over the
|
|
# internal compose network. Uncomment to inspect Qdrant directly.
|
|
# ports:
|
|
# - "6333:6333"
|
|
|
|
mempalace:
|
|
image: ghcr.io/mempalace/mempalace:latest
|
|
restart: unless-stopped
|
|
depends_on:
|
|
- qdrant
|
|
command:
|
|
- serve
|
|
- --host
|
|
- "0.0.0.0"
|
|
- --port
|
|
- "8765"
|
|
- --backend
|
|
- qdrant
|
|
# Uncomment to expose recall without write access to most clients:
|
|
# - --read-only
|
|
environment:
|
|
# Required for the network-exposed bind. Set in deploy/.env.
|
|
MEMPALACE_MCP_HTTP_TOKEN: ${MEMPALACE_MCP_HTTP_TOKEN:?set MEMPALACE_MCP_HTTP_TOKEN in deploy/.env}
|
|
MEMPALACE_QDRANT_URL: http://qdrant:6333
|
|
MEMPALACE_QDRANT_API_KEY: ${MEMPALACE_QDRANT_API_KEY:-}
|
|
# Set to cuda/dml/coreml on an accelerated host (see Dockerfile.gpu).
|
|
MEMPALACE_EMBEDDING_DEVICE: ${MEMPALACE_EMBEDDING_DEVICE:-auto}
|
|
ports:
|
|
- "8765:8765"
|
|
volumes:
|
|
- mempalace-data:/data
|
|
healthcheck:
|
|
# The image has no curl; use Python (always present). /healthz needs no auth.
|
|
# If you enable TLS on the server itself, switch this to https + ssl context.
|
|
test:
|
|
- CMD
|
|
- python
|
|
- -c
|
|
- "import urllib.request,sys; sys.exit(0) if urllib.request.urlopen('http://127.0.0.1:8765/healthz').read().strip()==b'ok' else sys.exit(1)"
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 40s
|
|
|
|
volumes:
|
|
qdrant-storage:
|
|
mempalace-data:
|