Skip to content

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.

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.

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:

Terminal window
keelson connect claude # or: copilot, codex, or 'all'

It handles three agents today, each wired at its native location:

AgentHow it is wired (global)Transport
claudeclaude mcp add --scope user (user scope, in ~/.claude.json)HTTP
copilot~/.copilot/mcp-config.jsonHTTP
codex~/.codex/config.tomlthe 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:

Terminal window
keelson connect --list # show what is connected
keelson disconnect claude # or: keelson connect claude --undo

Disconnect 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.

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.

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.

Four settings narrow the endpoint, in ~/.keelson/config.json under mcp:

  • exposeStateChanging: false restricts the endpoint to read-only tools, hiding workflow_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=0 forces it off).
  • requireToken: true gates every request behind the bearer token in server.json. Off by default, since the bind is already loopback-only.
  • toolDenylist removes named tools, whatever the flags above allow.
  • enabled: false (or KEELSON_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.