feat(servers): persistent volumes + self-hosted web search and docs templates
Instances are immutable and get recreated on any server edit, so anything an MCP server wrote to its container filesystem was lost at exactly that point. That ruled out every stateful MCP server, docs-mcp among them: its index is a SQLite file (better-sqlite3 + sqlite-vec) and it has no external-database mode, so no amount of Postgres helps. A server or template can now declare volumes. The backing store is keyed on the server, not the instance — `mcpctl-<server>-<name>` — which is the whole point: an instance-scoped claim would be destroyed precisely when the data needs to survive. On Kubernetes that is a PVC ensured in the servers namespace before the pod is created and never deleted with it; on Docker, a named volume (named, not anonymous, so `removeContainer`'s `v: true` leaves it alone). Claims are ReadWriteOnce, so volumes and replicas > 1 are mutually exclusive; validation rejects that pair instead of leaving the extra replicas unschedulable. storageClassName is omitted rather than sent empty when no class is configured — to Kubernetes those mean different things. Also fixes a pre-existing bug in the same path: seedTemplates dropped `runtime`, so every PyPI-backed template seeded from YAML silently defaulted to node and would run `npx` against a package that only exists on PyPI. `unifi-network` declares `runtime: python` and had been seeding with runtime unset. Templates added, all self-hosted and none needing an API key: - duckduckgo — no backing service at all - searxng — needs a SearXNG engine (compose profile in stack/) - docs-mcp — open-source Context7/Ref alternative, uses the new volume Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB
This commit is contained in:
@@ -3,3 +3,8 @@ POSTGRES_PASSWORD=CHANGE_ME
|
||||
POSTGRES_DB=mcpctl
|
||||
MCPD_PORT=3100
|
||||
MCPD_LOG_LEVEL=info
|
||||
|
||||
# --- websearch profile (docker compose --profile websearch up -d) ---
|
||||
# The SearXNG engine only. The MCP servers in front of it are mcpctl
|
||||
# resources, not compose services. No API key needed.
|
||||
SEARXNG_SECRET=CHANGE_ME
|
||||
|
||||
@@ -48,6 +48,39 @@ services:
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
|
||||
# --- websearch profile -------------------------------------------------
|
||||
# Opt-in: `docker compose --profile websearch up -d`.
|
||||
#
|
||||
# Only the SearXNG *engine* lives here — it is plain infrastructure, not an
|
||||
# MCP server, so mcpctl has nothing to manage it with. The MCP servers that
|
||||
# sit in front of it (`searxng`, `duckduckgo`, `docs-mcp`) are mcpctl
|
||||
# resources created from templates. Needs no API key.
|
||||
|
||||
searxng:
|
||||
image: docker.io/searxng/searxng:latest
|
||||
container_name: mcpctl-searxng
|
||||
profiles: ["websearch"]
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
SEARXNG_BASE_URL: http://mcpctl-searxng:8080/
|
||||
SEARXNG_SECRET: ${SEARXNG_SECRET:-mcpctl-searxng-internal-only}
|
||||
# The limiter guards public instances from bots and needs Valkey to work.
|
||||
# This one binds no host port and is reachable only from mcp-servers.
|
||||
SEARXNG_LIMITER: "false"
|
||||
volumes:
|
||||
- ./searxng/settings.yml:/etc/searxng/settings.yml:ro
|
||||
- mcpctl-searxng-cache:/var/cache/searxng
|
||||
networks:
|
||||
- mcp-servers
|
||||
healthcheck:
|
||||
# Probes the JSON API specifically — a healthy HTML UI with `json` missing
|
||||
# from search.formats is exactly the failure mode worth catching here.
|
||||
test: ["CMD-SHELL", "wget -q -O /dev/null 'http://localhost:8080/search?q=ping&format=json' || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 20s
|
||||
|
||||
networks:
|
||||
mcpctl:
|
||||
driver: bridge
|
||||
@@ -60,3 +93,4 @@ networks:
|
||||
volumes:
|
||||
mcpctl-pgdata:
|
||||
mcpctl-backup:
|
||||
mcpctl-searxng-cache:
|
||||
|
||||
14
stack/searxng/settings.yml
Normal file
14
stack/searxng/settings.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
# Minimal SearXNG config for API/MCP use. Everything not set here inherits the
|
||||
# image defaults (engines, locales, categories) via use_default_settings.
|
||||
#
|
||||
# secret_key and limiter come from $SEARXNG_SECRET / $SEARXNG_LIMITER in the
|
||||
# environment — see stack/docker-compose.yml. `formats` has no env override,
|
||||
# which is the only reason this file has to exist.
|
||||
use_default_settings: true
|
||||
|
||||
search:
|
||||
# The upstream default is `[html]` only. Without `json` here every
|
||||
# /search?format=json request 403s and mcp-searxng returns nothing.
|
||||
formats:
|
||||
- html
|
||||
- json
|
||||
Reference in New Issue
Block a user