Using keelson over MCP
Keelson exposes its tool registry over the Model Context Protocol,
so another MCP-capable agent (Claude Code, Cursor, the Copilot CLI, the Codex CLI)
can call your installed ribs’ tools and the workflow tools. The endpoint comes up
with the server, so keelson start is the only setup it needs.
Tools run inside the keelson server, where each rib keeps its credentials and exec access. An external agent that calls a rib’s tool gets the rib’s real capability, not a reimplementation, and never sees the secrets behind it.
What is exposed
Section titled “What is exposed”The gateway serves the same registry the chat agent reads: every active rib’s
tools, plus the harness’s own. Six of those are read-only and always exposed,
present even with zero ribs installed and unaffected by exposeStateChanging:
keelson_docs, workflow_list, workflow_status, run_list, run_status, and
run_events. By default the gateway exposes the mutating surface too:
workflow_run, workflow_respond, workflow_resume, workspace_lease,
workspace_release, run_cancel, and run_steer, plus a rib’s mutating tools,
served alongside the read-only ones. Restrict it to read-only with
exposeStateChanging: false when you want a narrower surface. A hidden or denied
tool is reported as unknown, so the endpoint never reveals what it is
withholding.
Tool input schemas are emitted with z.toJSONSchema(..., { reused: "ref" }), so
a sub-schema used more than once lands in $defs and is referenced by $ref
rather than inlined. A client that does not resolve $ref will break on those
tools. tools/list also sets the readOnlyHint and destructiveHint
annotations, so a client can tell the two halves of the registry apart without
calling anything.
An orchestrating agent should poll a workflow with MCP workflow_status and
brief: true. The compact result includes each node’s id and status, live
current, the awaiting node, and pauseId, without returning every node’s
output again. Those fields preserve the handle needed to resume an approval
gate. The CLI workflow status --brief is a smaller operator view: it lists the
nodes and names only the awaiting node as current, but it does not return
pauseId. Do not use the CLI brief for an agent loop that must resume approvals.
Connect an agent with one command
Section titled “Connect an agent with one command”keelson connect wires a coding agent to the endpoint for you. It registers the
MCP server in that agent’s own config and drops a small, portable skill that
teaches the agent when to reach for Keelson. Writes are machine-global by default,
so the connection follows you into every repo. Run it once from anywhere:
keelson connect claude # or: copilot, codex, or 'all'It handles three agents today, each wired at its native location:
| Agent | How it is wired (global) | Transport |
|---|---|---|
claude | claude mcp add --scope user (user scope, in ~/.claude.json) | HTTP |
copilot | ~/.copilot/mcp-config.json | HTTP |
codex | ~/.codex/config.toml | the keelson mcp stdio bridge |
Claude has no dedicated global MCP file: its user scope lives inside
~/.claude.json, its whole state file, so Keelson drives Claude’s own claude mcp
CLI rather than hand-editing it. Codex speaks only stdio, so it gets the bridge
instead of the HTTP URL; the bridge resolves the server URL and token on each launch,
so the wiring survives a restart or a non-default port.
The skill lands in each agent’s own skills directory: Copilot and Codex share
~/.agents/skills/keelson/SKILL.md (both read it), and Claude reads its own
~/.claude/skills/keelson/SKILL.md. It names no rib and no rib-specific tool: it
points the agent at keelson_docs and the workflow tools, so it stays correct as
you install or remove ribs.
Prefer to commit the wiring into a repo? keelson connect claude --local writes the
repo-scoped equivalents instead: a project .mcp.json for Claude, and the skill
under the repo’s .claude/skills for Claude or .agents/skills for Copilot and
Codex. Teammates then inherit it through version control.
keelson connect records what it wrote, so reversing it is exact:
keelson connect --list # show what is connectedkeelson disconnect claude # or: keelson connect claude --undoDisconnect removes only Keelson’s own entry, never a sibling MCP server or a file
you already had. Pass --no-skill to wire the connection without the skill, or
--url to write a non-default endpoint.
That record is the only thing that knows which agents are wired, so no command
treats one it cannot parse as an empty ledger. connect, disconnect, and
connect --list stop with a BAD_RECEIPT error and leave the file untouched.
uninstall reports that it disconnected nothing, finishes removing everything
else, and exits non-zero. A connect that overwrote a corrupt receipt would drop
the entries for agents already connected, leaving them pointing at Keelson with
nothing left to reverse them. If you see BAD_RECEIPT, repair
<home>/connections.json, or delete it and accept that any agent already wired
must then be unwired with that agent’s own CLI.
Connect a client by hand
Section titled “Connect a client by hand”For a client keelson connect does not cover, or to wire one yourself, point an
HTTP MCP client straight at the endpoint:
// Cursor or another HTTP MCP client{ "mcpServers": { "keelson": { "type": "http", "url": "http://127.0.0.1:7878/api/mcp" } }}The endpoint is JSON-only: it answers POST with a single JSON-RPC reply and
answers any non-POST method with 405, since there is no server-to-client event
stream.
Any client that connects receives the server’s initialize-time instructions,
which point it at keelson_docs and the workflow tools, so even a hand-wired,
skill-less client gets baseline orientation on where to look next.
Bridge a stdio-only client
Section titled “Bridge a stdio-only client”Some clients speak only stdio MCP. keelson mcp is a bridge: it reads JSON-RPC on
stdin, forwards each message to the running server’s /api/mcp, and writes the
reply to stdout. It exits non-zero when the server is down.
# Codex CLI (~/.codex/config.toml)[mcp_servers.keelson]command = "keelson"args = ["mcp"]The bridge follows the URL the running server recorded in server.json, so it
finds a non-default port on its own; pass --base-url to target a specific
server. When the endpoint is token-gated, the bridge sends the stored token only
to that recorded server, never to an explicit --base-url target.
Gate it
Section titled “Gate it”Four settings narrow the endpoint, in ~/.keelson/config.json under mcp:
exposeStateChanging: falserestricts the endpoint to read-only tools, hidingworkflow_run,workflow_respond,workflow_resume,workspace_lease,workspace_release,run_cancel,run_steer, and a rib’s mutating tools. On by default (KEELSON_MCP_EXPOSE_STATE_CHANGING=0forces it off).requireToken: truegates every request behind the bearer token inserver.json. Off by default, since the bind is already loopback-only.toolDenylistremoves named tools, whatever the flags above allow.enabled: false(orKEELSON_MCP_DISABLED=1) takes the endpoint down entirely.
// ~/.keelson/config.json: restrict to read-only, behind a token{ "mcp": { "exposeStateChanging": false, "requireToken": true }}Each setting has a KEELSON_MCP_* environment override that wins over the config
value; Configuration is the full table.
Related
Section titled “Related”- Configuration: the
mcpconfig block and theKEELSON_MCP_*environment overrides. - The CLI: the
keelson mcpstdio bridge. - The HTTP and WS API:
/api/mcpalongside the rest of the local surface. - Managing ribs: the ribs whose tools the gateway exposes.