Liking cljdoc? Tell your friends :D

MCP: dvergr from Claude Code, Codex, Cursor and other clients

dvergr serves the Model Context Protocol so that a coding client can hand work to it: run a task on forks of a room, evaluate Clojure in the room's REPL, adopt a result by merge, and see what it cost. The tools are derived from dvergr.ops/specification (and the room-scoped dvergr.tools registry); a new op appears in MCP, the JSON API and the web without extra code.

Connecting a client

A client launches bin/dvergr-mcp (babashka) as a stdio server. It relays to the daemon's loopback TCP port (default 127.0.0.1:17888):

  • when no daemon listens, it starts one (clojure -M:cli --no-tui --mcp …, log in .dvergr/logs/mcp-daemon.log) and answers initialize itself meanwhile, from the last tool list it saw (.dvergr/mcp/), so clients with short startup timeouts connect;
  • when the daemon restarts, it reconnects, replays the handshake and sends tools/list_changed; a call in flight gets an error result saying the connection dropped;
  • it never starts a second daemon on a state root whose daemon (.dvergr/daemon.pid) runs without MCP; restart that one with --mcp, or set :mcp in its config.

Claude Code (.mcp.json or claude mcp add):

{"mcpServers": {"dvergr": {"type": "stdio",
                           "command": "/path/to/dvergr/bin/dvergr-mcp",
                           "args": ["--profile", "offload"]}}}

Codex (~/.codex/config.toml):

[mcp_servers.dvergr]
command = "/path/to/dvergr/bin/dvergr-mcp"
startup_timeout_sec = 30
tool_timeout_sec = 300

Options (flags or environment): --profile / DVERGR_MCP_PROFILE, --toolsets a,b / DVERGR_MCP_TOOLSETS, --port / DVERGR_MCP_PORT, --dir / DVERGR_DIR (the checkout the daemon starts in), DVERGR_HOME (state root), DVERGR_MCP_START (the start command), --no-start, --wait-s.

To run the daemon yourself: clojure -M:cli --no-tui --mcp [--mcp-port 17888], or :mcp {:port 17888 :profile "offload"} in the config file. The daemon inherits its environment: a TELEGRAM_BOT_TOKEN there connects that bot.

Profiles and toolsets

A connection sees one selection of tools, chosen by --profile / --toolsets (the relay puts them into initialize _meta as dvergr/profile and dvergr/toolsets). Tools outside it are neither listed nor callable.

ProfileToolsetsTools
offload (default)rooms, worlds, attempts, catalog, wallets, repl26
readonlyevery read opread-only only
admineverythingall
ToolsetWhat
roomscreate, list, detail, delete, messages, post
worldsfork, review, diff, merge, discard
attemptsworkflow_start and the job tools; Attempts, Scorecards
runsthe blocking workflow_attempt; Runs
catalogcatalog_list, catalog_start: workflows with their own checker
walletsroom_wallet, models_list
replclojure_eval in the room's SCI sandbox
agentsagent administration, room_invite
systemstatistics
codefiles, search, shell, tasks: for hosts without their own (off by default)
extratools registered at runtime (channels)

The default is small on purpose: some clients cap the tools of all their servers together around 40, and every definition costs context in every session.

What each tool promises

  • Handles, not sessions. Every call names its room (id or slug) or fork; the server keeps no per-connection state beyond the selection. (MCP 2026-07-28 removes protocol sessions; this surface already fits it.)
  • A coding tool always runs in a room: room is required, and the tool runs in that room's sandbox and workspace, never in the daemon's own directory.
  • Annotations on every tool (readOnlyHint, destructiveHint, idempotentHint, openWorldHint, title), from the op's :kind and the tables in dvergr.mcp.surface. Clients use them for confirmation: clojure_eval, room_merge and room_discard are destructive, so e.g. Codex asks before calling them.
  • Results are JSON text plus the same value as structuredContent; text beyond 60k characters is cut with a note (Claude Code caps a result at 25k tokens).
  • Schemas are flat and closed (additionalProperties: false, no oneOf/anyOf/$ref) and names match ^[a-z][a-z0-9_]{0,39}$, so strict model APIs accept them; server_test checks every tool.
  • Long work is a job. workflow_start returns a job at once; job_status {job, wait-ms} answers when it finishes or after at most 25 s (below every common client timeout), and job_cancel stops it. A job is a durable Run of its room, and the attempts it starts are its child Runs, so a client can restart and poll again (job_list), and a job outlives the daemon: after a restart it reads as it finished, or failed with reason orphaned if the daemon stopped while it ran. A workflow runs up to parallelism attempts at once (default 4).
  • A room's reads are live. Every read op is also a resource (room://{room}/messages, experiment://{room}/progress, …). Subscribing to one about a room sends notifications/resources/updated after every durable change of that room (a message, a Run, a tool call, an Attempt), at most a few times a second; re-read the resource for what changed.
  • Your code in, a patch out. room_import {room, source} creates a room whose workspace is a clone of a local checkout (its committed state, with history) or an https remote. Work on it with workflow_start, adopt an attempt with room_merge, then room_export {room} returns the changes since the import as a patch: git apply it in your checkout. Only these two ops reach a repository outside the room; a room's own sandbox Git has no network transport.
  • Merges are pinned to the review. room_review (and every workflow attempt's review) carries the fork's state, a hash of its systems' snapshot ids; room_merge {room, expect-state} refuses a fork that changed since. A failed merge is an error, not a success.
  • A merge adopts a fork's files. A workspace change is only in the diff once committed; an attempt's world is committed when its Run ends, and room_merge commits a fork's remaining changes before merging (room_review lists them as uncommitted, and they make a fork reviewable). Without this a merge silently dropped uncommitted files.
  • Catalog workflows are scored by their own checker. catalog_start {workflow: "wiki/v1"} without a room runs on the benchmark set (fixtures seeded into a new room, ten known facts); with a room, on its own /docs, scored by citations, links and structure. Each attempt's checks say what it got right (one per fact), reward weighs them.
  • Money is in the result. A workflow result has each attempt's spend, the per-model table (cost per completed attempt) and the room's wallet afterwards.
  • REPL definitions live in the daemon's memory: they do not survive a daemon restart. Keep lasting code in the room's workspace.

Protocol

Handshake versions 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25; an unknown version is answered with the latest. Tools, resources (derived from the read ops) with subscriptions, and listChanged. Unknown tools are -32602.

  1. repl_describe (search the bound API) next to clojure_eval; evals metered to the wallet; a code-mode profile.
  2. MCP 2026-07-28 (stateless, server/discover, _meta per request) and Streamable HTTP for hosted use, then OAuth.
  3. A data toolset over pg-datahike (load a pg_dump, migrate on a fork, merge), and a client test pass over Cursor, VS Code, Gemini CLI, n8n and ChatGPT.

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close