Famulus MCP

The hierarchical MCP gateway: one entry for every client, a live catalog of all backends, search and forwarding

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 gerd plus the Keychain password (service famulus-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

  1. Run it as its own process: an HTTP service under launchd with /mcp, or a stdio command.
  2. Add platform/registry/mcp/<name>/mcp.yaml with name, description, transport, url or command/args/cwd/env, and the gateway block: namespace, tier, enabled, lifecycle, tool_prefix, pinned, timeout, and optionally direct.
  3. Restart the gateway, or call mcp_refresh. The tools appear in the catalog; the registry does not list them by hand.
  4. Run ae-platform mcp sync only 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.