Built-in Model Context Protocol (MCP) surface. The gateway daemon owns ONE
shared pool of MCP connections ({server {:conn spec}}); every session sees
the same live tools. Always on: MCP is core infrastructure, not a droppable
plug-in and not gated by any toggle. The pool is empty (and costs nothing)
until at least one server is declared in config.
Servers are declared natively in ~/.vis/state.yml:
{:mcp {:servers {"filesystem" {:transport :stdio :command "npx" :args ["-y" "@modelcontextprotocol/server-filesystem" "/path"]} "remote" {:transport :streamable-http :url "https://.../mcp" :headers {"Authorization" "Bearer ${MY_TOKEN}"} :timeout_ms 60000} "stale" {:enabled false :url "https://.../mcp"}}}}
Every string in :headers / :env / :args / :url / :command / :cwd
supports ${ENV_VAR} interpolation from the host environment. :enabled false skips the server without deleting the entry. HTTP servers with no
static bearer transparently negotiate OAuth 2.1 on first 401 (RFC 9728
discovery + RFC 7591 dynamic client registration + PKCE loopback).
ONE model-facing verb under alias mcp (flat sandbox renders alias_name):
mcp__call(server, tool, args) - call a tool
mcp__call(server) - that server's descriptions + input schemas
There is deliberately NO connect/disconnect verb. The daemon connects every enabled server, health-checks the pool on its own clock, and reaps/respawns a dead one; a tool call self-heals its connection too. Starting or stopping a server is a human admin action on the gateway API (save/enable/kill/start), never something one session does to a resource every other session shares.
Every visible server - its status and the NAMES of the tools it exposes -
rides in ctx under env.mcp, keyed by server name so a change diffs per
server. That IS the inventory: no listing verb spends a turn re-fetching what
the session object already carries.
Built-in Model Context Protocol (MCP) surface. The gateway daemon owns ONE
shared pool of MCP connections (`{server {:conn spec}}`); every session sees
the same live tools. Always on: MCP is core infrastructure, not a droppable
plug-in and not gated by any toggle. The pool is empty (and costs nothing)
until at least one server is declared in config.
Servers are declared natively in `~/.vis/state.yml`:
{:mcp {:servers {"filesystem" {:transport :stdio :command "npx"
:args ["-y" "@modelcontextprotocol/server-filesystem" "/path"]}
"remote" {:transport :streamable-http :url "https://.../mcp"
:headers {"Authorization" "Bearer ${MY_TOKEN}"}
:timeout_ms 60000}
"stale" {:enabled false :url "https://.../mcp"}}}}
Every string in `:headers` / `:env` / `:args` / `:url` / `:command` / `:cwd`
supports `${ENV_VAR}` interpolation from the host environment. `:enabled
false` skips the server without deleting the entry. HTTP servers with no
static bearer transparently negotiate OAuth 2.1 on first 401 (RFC 9728
discovery + RFC 7591 dynamic client registration + PKCE loopback).
ONE model-facing verb under alias `mcp` (flat sandbox renders `alias_name`):
mcp__call(server, tool, args) - call a tool
mcp__call(server) - that server's descriptions + input schemas
There is deliberately NO connect/disconnect verb. The daemon connects every
enabled server, health-checks the pool on its own clock, and reaps/respawns a
dead one; a tool call self-heals its connection too. Starting or stopping a
server is a human admin action on the gateway API (save/enable/kill/start),
never something one session does to a resource every other session shares.
Every visible server - its status and the NAMES of the tools it exposes -
rides in ctx under `env.mcp`, keyed by server name so a change diffs per
server. That IS the inventory: no listing verb spends a turn re-fetching what
the session object already carries.(cancel-gateway-server-auth! flow-id)Forget an abandoned flow and release its listener.
Forget an abandoned flow and release its listener.
(clear-session-servers! session-id)Drop and CLOSE every session-scoped server attached to session-id.
Drop and CLOSE every session-scoped server attached to `session-id`.
(complete-gateway-server-auth! flow-id input)Finish flow flow-id with the redirect URL the user pasted back (or a bare
authorization code) and reconnect the server.
Finish flow `flow-id` with the redirect URL the user pasted back (or a bare authorization code) and reconnect the server.
(delete-gateway-server! name)Remove a server from this gateway's machine-owned state and stop it now.
Remove a server from this gateway's machine-owned state and stop it now.
(gateway-server-auth-status name)Non-secret OAuth state for name: whether tokens exist, whether they expired.
Non-secret OAuth state for `name`: whether tokens exist, whether they expired.
(gateway-servers)Sanitized MCP inventory for gateway management. Secrets (env and header values)
deliberately never cross this boundary. Every configured server is listed —
gateway-owned and hand-written alike — and is_managed says which of them
this API may write. Reading the inventory also nudges the pool toward config
and arms the health loop, so a client that has not run a turn yet still gets —
not merely sees — the connections the gateway owes it.
String-keyed throughout, like every MCP surface.
Sanitized MCP inventory for gateway management. Secrets (env and header values) deliberately never cross this boundary. Every configured server is listed — gateway-owned and hand-written alike — and `is_managed` says which of them this API may write. Reading the inventory also nudges the pool toward config and arms the health loop, so a client that has not run a turn yet still gets — not merely sees — the connections the gateway owes it. String-keyed throughout, like every MCP surface.
(kill-gateway-server! name)Stop name NOW and keep it stopped: close the connection (for stdio that
destroys the child process, forcibly if it will not go) and set the kill brake
so the per-turn reconcile does not respawn it. Nothing is persisted — use
set-gateway-server-enabled! for a durable off switch. Works for hand-written
servers too: killing a runaway process is not editing the user's file.
Stop `name` NOW and keep it stopped: close the connection (for stdio that destroys the child process, forcibly if it will not go) and set the kill brake so the per-turn reconcile does not respawn it. Nothing is persisted — use `set-gateway-server-enabled!` for a durable off switch. Works for hand-written servers too: killing a runaway process is not editing the user's file.
(logout-gateway-server-auth! name)Forget the persisted OAuth tokens for name and drop the connection that was
using them.
Forget the persisted OAuth tokens for `name` and drop the connection that was using them.
(mcp-call server)(mcp-call server tool)(mcp-call server tool args)Invoke tool on configured MCP server server with args matching its input schema (omit or {} for none): {"server": S, "tool": S, "content": [<MCP content blocks>], "is_error": bool}; text is at content[i]["text"], and a refused call carries "input_schema" back with it. Called with server alone it lists that server's catalog instead: {"server": S, "tools": [{"name": S, "description": S, "input_schema": <JSON schema dict>}]}. Server names, status and tool names already ride in ctx under env.mcp; the gateway connects and heals every enabled server on its own clock, so there is no connect/disconnect verb. Config: ~/.vis/state.yml :mcp :servers.
Invoke `tool` on configured MCP server `server` with `args` matching its input schema (omit or {} for none): {"server": S, "tool": S, "content": [<MCP content blocks>], "is_error": bool}; text is at content[i]["text"], and a refused call carries "input_schema" back with it. Called with `server` alone it lists that server's catalog instead: {"server": S, "tools": [{"name": S, "description": S, "input_schema": <JSON schema dict>}]}. Server names, status and tool names already ride in ctx under env.mcp; the gateway connects and heals every enabled server on its own clock, so there is no connect/disconnect verb. Config: ~/.vis/state.yml :mcp :servers.
(poll-gateway-server-auth! flow-id)Non-blocking verdict for flow-id: pending, ok, or error. This is how a
client learns the loopback listener already finished the flow for it.
Non-blocking verdict for `flow-id`: `pending`, `ok`, or `error`. This is how a client learns the loopback listener already finished the flow for it.
(save-gateway-server! name raw-spec)Validate and persist a complete string-keyed server spec in this gateway's machine state, then reconnect it in the background. Returns its sanitized row.
env and headers survive a save that omits them: see with-preserved-secrets.
Validate and persist a complete string-keyed server spec in this gateway's machine state, then reconnect it in the background. Returns its sanitized row. `env` and `headers` survive a save that omits them: see `with-preserved-secrets`.
(session-servers session-id)The session-scoped servers attached to session-id as string-keyed rows
[{name, transport, is_connected}], in name order — like every MCP surface.
The session-scoped servers attached to `session-id` as string-keyed rows
`[{name, transport, is_connected}]`, in name order — like every MCP surface.(set-gateway-server-enabled! name enabled)Persist an enabled/disabled override without exposing or accepting secrets.
Persist an enabled/disabled override without exposing or accepting secrets.
(set-session-servers! session-id servers)Attach servers — {name raw-spec}, each raw spec shaped exactly like a
:mcp :servers config entry — to session-id, REPLACING whatever that
session had, and connect each one EAGERLY so the caller learns now whether the
client's servers actually work. Nothing is persisted. Returns
{connected [name…], failed [{server …, error …}…]} — STRING-keyed, like every
MCP surface. A detach/replacement
racing a handshake wins: the just-opened transport is closed, never leaked.
Attach `servers` — `{name raw-spec}`, each raw spec shaped exactly like a
`:mcp :servers` config entry — to `session-id`, REPLACING whatever that
session had, and connect each one EAGERLY so the caller learns now whether the
client's servers actually work. Nothing is persisted. Returns
`{connected [name…], failed [{server …, error …}…]}` — STRING-keyed, like every
MCP surface. A detach/replacement
racing a handshake wins: the just-opened transport is closed, never leaked.(start-gateway-server! name)Undo a kill: release the brake and connect name right now. A disabled server
stays down — enabled false is the user's decision, not a stale brake.
Undo a kill: release the brake and connect `name` right now. A disabled server stays down — `enabled false` is the user's decision, not a stale brake.
(start-gateway-server-auth! name)Begin a headless OAuth 2.1 flow for HTTP server name. Returns
{flow_id, server, kind, url, redirect_uri, expires_at_ms, status} —
string-keyed, like every MCP surface. The caller shows url and the user
authorizes in their own browser.
Begin a headless OAuth 2.1 flow for HTTP server `name`. Returns
`{flow_id, server, kind, url, redirect_uri, expires_at_ms, status}` —
string-keyed, like every MCP surface. The caller shows `url` and the user
authorizes in their own browser.(test-gateway-server! name raw-spec)Connect a candidate spec without saving it. The connection is always closed; only non-secret tool metadata is returned, string-keyed like the inventory.
Connect a candidate spec without saving it. The connection is always closed; only non-secret tool metadata is returned, string-keyed like the inventory.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |