Famulus MCP
Every MCP client on this Mac (Cursor, Claude Code, Zed, VS Code) attaches one server: famulus-mcp at http://127.0.0.1:5090/mcp. Behind it stand all MCP backends of the platform. Each backend keeps its own process and its own data. The gateway only knows them, much as a DuckLake catalog knows its collections without holding their Parquet files.
Why a hierarchy
Until October 2026 every client wired every server itself, N servers × M clients:
- every new server had to be entered in every client;
- every stdio server ran once per editor (hama twice under Claude Desktop);
- each editor process held its own DuckDB connections, so the korrespondenz MCP could block the privatkanzlei sync.
The gateway turns this into a tree:
Clients (Cursor · Claude Code · Zed · VS Code)
│ one entry: famulus-mcp http://127.0.0.1:5090/mcp
▼
Famulus MCP gateway (launchd com.ai-platform.mcp-gateway, port 5090)
catalog: tier → namespace → tool (live tools/list of every backend, platform/mcp_catalog.duckdb)
│
├── privat korrespondenz (:5301) mail (:5310) biblio (stdio, per call) finanzen (stdio, per call)
├── famulus reference (:5135, DuckLake) hama (stdio, persistent) causal (stdio, per call)
├── sprache lwgrammar (:5116)
└── platform models (:5119) platform_* (agents, skills, instructions, projects, ports)
| DuckLake | Famulus MCP |
|---|---|
catalog (famulus.ducklake) |
registry platform/registry/mcp/*/mcp.yaml + live catalog mcp_catalog.duckdb |
| schema per collection | namespace per backend, grouped in tiers |
| table | tool (namespace.tool) |
| data files stay in the collection | each backend keeps its process and its data |
readers ATTACH one catalog |
clients attach one gateway |
How a client finds and uses a tool
The gateway does not flatten the roughly 160 tools into one list; that would fill every client’s context. Instead it offers four steps, plus a few pinned tools:
| Tool | Purpose |
|---|---|
mcp_catalog(path) |
The tree. '' gives tiers and namespaces with health; a namespace gives its tools, one line each |
mcp_find(query, tier?, namespace?) |
Ranked tools for a request in German or English, with input schemas |
mcp_describe(tool) |
One tool in full |
mcp_call(tool, arguments) |
Forwards to the backend; the result comes back unchanged, with errors marked as isError |
| pinned tools | Frequent tools re-exported at the top level (registry pinned): korrespondenz_mail_search, korrespondenz_mail_thread, korrespondenz_mail_read, fm_sent_search, fm_contact_get, lw_term |
platform_* |
The platform catalog: agents, skills, instructions, projects and ports |
mcp_refresh(namespace?) |
Re-read the tool lists (the gateway does this itself every 10 minutes) |
Tools that send mail keep their own confirm flag; the gateway never adds or removes one.
How backends run
The registry field lifecycle decides how the gateway reaches a backend:
http: a service of its own under launchd. The gateway opens one short MCP session per request. Used by korrespondenz, mail, reference, lwgrammar and models.per-call: a stdio server started for one request and ended afterwards, so no file lock outlives a call. Used by biblio, finanzen and causal.persistent: a stdio server the gateway starts once and keeps; a dead child is restarted on next use. Used by hama.
Every backend has a timeout and a circuit breaker. After a failure it is reported as down, with its error, and retried after a back-off (30 s, doubling up to 10 min). A down backend never appears as an empty result.
The korrespondenz service
korrespondenz runs as com.privatkanzlei.korrespondenz-mcp on 127.0.0.1:5301 and no longer depends on an editor:
- One call, one process. Every call runs in its own short-lived worker process (
korrespondenz.mcp_worker), so no DuckDB connection outlives a call and the privatkanzlei sync is never blocked. - Read-only where possible. Read tools open the archive read-only.
- Explicit lock message. While the sync holds the archive, a call waits 25 s and then answers that the archive is locked, never with an empty result.
- Crashes stay contained. A crashing or hanging tool is killed and reported; the service keeps running.
Access
- This Mac (loopback, no proxy headers): no login.
- Other machines on the tailnet: HTTP Basic
gerdplus the Keychain password (servicefamulus-mcp). The tier privat is neither listed nor forwarded to them. - Proxy: the gateway has no proxy route, so it is not reachable through the public tunnel.
Registering a new server
- Run it as its own process: an HTTP service under launchd with
/mcp, or a stdio command. - Add
platform/registry/mcp/<name>/mcp.yamlwithname,description,transport,urlorcommand/args/cwd/env, and the gateway block:namespace,tier,enabled,lifecycle,tool_prefix,pinned,timeout, and optionallydirect. - Restart the gateway, or call
mcp_refresh. The tools appear in the catalog; the registry does not list them by hand. - Run
ae-platform mcp synconly if the server should also get a direct client entry (direct: true).
Commands
ae-platform mcp list # live status of every backend, from the gateway
ae-platform mcp sync # client configs from the registry (Cursor, Claude Code, Zed, VS Code), with backups
ae-platform mcp serve # the gateway in the foreground (normally launchd com.ai-platform.mcp-gateway)
curl -s 127.0.0.1:5090/health | jq .The hub page /admin/mcp shows the same live status per tier and namespace.
Claude Desktop is not managed, because it runs only stdio servers from its config. Its own hama entry still starts hama under Claude Desktop. The 13 server-pdf processes come from the Claude Code pdf-viewer plugin, one per session, and are outside the platform’s control.