surface
Thirty tools, and a hatch to everything else
Twenty-six tools do concrete work. The remaining four are a door: they list, search, describe and invoke the tools of other MCP servers, so the catalogue this gateway exposes can grow without its own surface growing at all.
Registration is unremarkable and that is the point. Eight modules, eight
register*(server) functions, one barrel file that calls them in a fixed order. Every
tool is a Zod input schema and an async handler that returns MCP content. There is no middleware
chain, no per-tool permission table, no capability negotiation.
actions_shell_run executes arbitrary shell on the gateway box. Both
sit behind the same single bearer token — see the gateway.
The catalogue
Argument names and constraints below are taken from the Zod schemas. Optional arguments are
marked with ?; where a default exists it is stated.
| Tool | Arguments | Reaches |
|---|---|---|
wiki_index read | none | Disk — one line per page |
files_search read | query, limit? 1–50 = 10, mode? deep / keyword / vector = vector | Search CLI subprocess |
files_neighbors read | slug, max_bytes? 1K–500K = 30K, snippet_chars? 80–2000 = 280 | Disk — whole-corpus scan |
wiki_read_all read | max_bytes? 1K–2M = 1M | Disk — every page |
files_read read | path, host? = local, max_bytes? 1–5M = 1M | head -c, locally or over SSH |
files_write write | path under $HOME, content, mode? overwrite / append | Disk, gateway box only |
files_wiki_ingest write | filename, plus exactly one of content or url | Disk (raw/), optional HTTP fetch |
wiki_append write | slug, content, links?, create_new? = false | Disk + git + journal |
graph_query read | question, dfs? = false, budget? 200–20,000 = 2,000 | Python module, 60 s cap |
graph_path read | from, to | Python module, 30 s cap |
graph_explain read | node | Python module, 30 s cap |
memory_recall read | query, limit? 1–50 = 10, bank? = personal, tags? | Memory service over HTTP |
memory_retain write | text ≥ 3 chars, tags?, bank? | Memory service over HTTP |
memory_reflect read | query, budget? low / medium / high = low, bank? | Memory service over HTTP |
machines_host_list read | include_offline? = false | Two device inventories, merged by IP then name |
machines_access_guide read | host? | Same inventories; returns advice, not access |
machines_command_exec exec | host, command ≤ 20,000 chars, timeout_seconds? 1–600 = 60 | Five-step routing to a shell |
machines_file_copy exec | src_host, src_path, dst_host, dst_path | scp -3, 300 s cap |
machines_log_tail read | host, path, lines? 1–10,000 = 50 | tail -n via the same routing |
actions_github_run exec | args — an array of strings, never a shell string | GitHub CLI as the gateway's logged-in user |
actions_workspace_run exec | command, account? (two profiles), params?, json?, format? = json | Workspace CLI, config dir per account |
actions_shell_run exec | command ≤ 20,000 chars, timeout_seconds? 1–600 = 60 | bash -lc on the gateway box |
creds_token_list read | none | Alias names and references — never values |
creds_token_get exec | name, reason ≥ 3 chars | Vault CLI read; returns the raw secret |
creds_token_store write | name or a full vault reference, value, reason | Vault edit, falling back to create |
creds_token_rotate write | name, new_value, reason | Vault edit, logged as a rotation |
projects_list | none | In-memory downstream registry: status, tool count, last refresh |
projects_search | project?, query?, limit? 1–200 = 25, refresh? | Cached tool catalogues, substring match |
projects_schema | name — the prefixed tool name | The downstream tool's full input schema |
projects_call exec | name, args? | Proxied call to the owning downstream server |
Read the risk column, not the name
creds_token_get is tagged exec here rather than read deliberately:
it returns a live secret into the model's context. The tool description is explicit about the
contract — “The returned value is the raw secret — use it then drop it. Audit logs the alias
+ reason but never the value.” The reason argument is required, minimum three
characters, and is written to the audit ledger alongside the alias.
The discovery ladder, again
The four projects_* tools repeat the pattern the retrieval router established:
cheap discovery first, expensive action last. A model that wants a capability the gateway does
not implement directly climbs four rungs — and only the last one does anything.
- projects_list
- Which downstream servers exist, whether each is connected, how many tools it published, when the catalogue was last refreshed, and the last error if any.
- projects_search
- Substring match over cached
name + descriptionacross every downstream, optionally scoped to one namespace. Returns names and 200-character description excerpts — not schemas. - projects_schema
- The full input JSON schema for one prefixed tool name, fetched from the cache.
- projects_call
- Strips the prefix, resolves the owning client, reconnects it if the connection dropped, and forwards the call. The downstream's content and
isErrorflag are returned unmodified.
This is the same economics as wiki_index before wiki_read_all. Loading
every downstream schema into a tool listing would cost thousands of tokens per session for
capabilities most sessions never use. Instead the gateway publishes four tools and lets the model
pay for detail only when it needs detail.
src/federation/client.ts:15-18 spells out
that a literal bearer field exists but should be avoided in anything checked in.
Disabled is a first-class state
Entries in the downstream config carry an enabled flag, filtered at load time by
d.enabled !== false. The committed configuration uses it: several servers are present
but switched off, each with a sibling field recording the date and the reason it was disabled —
a port that is not listening, a redundancy after a tool was reimplemented natively, a transport
that returns HTML instead of completing a handshake.
That last one is why files_search shells out to a CLI instead of federating to the
search server that wraps the same index. The configuration file doubles as a decision log, and
the workaround in files.ts is the visible consequence of an entry in it.
A disabled entry with a written reason is worth more than a deleted one. It is the difference between a system that forgot and a system that decided.
on config/downstreams.json
What a federated call looks like on the wire
# 1 · which namespaces exist, and are they up? $ curl -s -X POST "$GATEWAY/mcp" -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call", "params":{"name":"projects_list","arguments":{}}}' {"projects": [{ "namespace": "<ns>", "prefix": "<ns>__", "status": "connected", "tool_count": 12, "last_refreshed": "…" }, … ]} # 2 · find a capability by keyword, without loading any schemas $ … -d '{"jsonrpc":"2.0","id":2,"method":"tools/call", "params":{"name":"projects_search","arguments":{"query":"email","limit":5}}}' {"total_matched": 3, "returned": 3, "tools": [{ "name": "<ns>__<tool>", … }]} # 3 · now pay for one schema $ … -d '{"…":"…","params":{"name":"projects_schema", "arguments":{"name":"<ns>__<tool>"}}}' # 4 · and invoke it — the prefix is stripped before it leaves the gateway $ … -d '{"…":"…","params":{"name":"projects_call", "arguments":{"name":"<ns>__<tool>","args":{…}}}}'
Response shapes are the exact objects serialised at src/tools/projects.ts:29-39 and
:82-90. Namespaces, tool names and the gateway address are placeholders.
One surface, many trust levels
The catalogue mixes a directory listing with arbitrary remote command execution and live
credential retrieval. There is no tiering: the same bearer token authorises
wiki_index and actions_shell_run. What separates them in practice is
that every call lands in the audit ledger, that the riskiest tools demand a written
reason, and that the instruction block asks the model to inspect before it writes.
Prefer discovery/search tools before broad calls. For risky or write actions, inspect first, keep the user informed, and avoid exposing secret values in normal replies.
src/server.ts:109 — the last line of the client instructions
Downstream identifiers, namespaces, URLs, credential aliases, vault names and account identities
from the committed configuration are omitted. <ns>, <tool>
and $GATEWAY are placeholders.