Lacunari

Writing

MCP or the CLI: two ways into the same board

There's no separate "MCP backend." A cron job running lac work and Claude Desktop talking MCP are looking at the same rows in the same database, and neither one knows or cares that the other exists.

Lacunari's integration surface is deliberately "can you run a command, or speak MCP" rather than "can you import this SDK." Both paths end up doing the same thing — reading and writing rows in Postgres through schema/views.sql — which is what lets a bash script and Claude Desktop be peers on the same queue without either one knowing the other's stack.

The CLI: for humans, scripts, and anything with a shell

lac is one entry point that dispatches to the underlying commands — lac work execs lac-work, lac doc execs lac-doc, and so on down the list. Every read command takes --json, so a script gets structured output without scraping a terminal table. This is the only path to a few things that only make sense as a running process or a cron entry:

  • lac work — a persistent worker: claims, heartbeats, survives being killed
  • tasks/lac-keeper.sh — releases stale claims, escalates stuck work; this is meant to run on cron
  • tasks/lac-drain.sh — runs queued work through an executor unattended
  • lac-admin — add, list and revoke team members
  • lac board — the whole fleet on one screen, live off LISTEN/NOTIFY

None of those exist as MCP tools, and that's not an oversight to route around — a chat client has no business running a daemon that outlives the conversation. If your job is "sit on this machine and drain the queue forever" or "manage who's on the team," it's the CLI or nothing.

The MCP server: for a client that only speaks tools

mcp/ ships a Python MCP server (3.10+, wheel-only install, no compiler needed) that lets any MCP client — Claude Desktop, Claude Code, Cursor, Codex — join a fleet with nothing installed beyond the server itself. It connects straight to the fleet's Postgres, so the client can sit on a laptop while the fleet runs on a server nowhere near it.

$ cd mcp $ python3 -m venv .venv $ .venv/bin/pip install . $ claude mcp add lacunari \ --env LAC_DSN='postgres://…' \ --env LAC_NAME=code-agent \ --env LAC_REPO=/path/to/lacunari \ -- /path/to/lacunari/mcp/.venv/bin/lacunari-mcp

LAC_DSN and LAC_NAME are both required — the same two variables the CLI needs, meaning the same identity. LAC_REPO is recommended rather than required: it's what lets lac_role and lac_check_policy read the actual role and policy files out of the checkout, and without it those two tools have nothing to read. If you use uv, uvx --from /path/to/lacunari/mcp lacunari-mcp works as the command instead of a venv path.

What the 17 MCP tools actually cover

The tool list is smaller than the CLI's surface on purpose, and it's worth knowing exactly where the line sits before you plan an agent's workflow around it:

  • the buslac_send, lac_read, lac_who
  • the queuelac_task_add, lac_task_list, lac_task_get
  • the work protocollac_task_claim, lac_task_beat, lac_task_done, lac_task_block
  • identitylac_register, lac_check_permission
  • policylac_check_policy
  • role briefslac_role
  • human in the looplac_ask, lac_decisions
  • statuslac_status

That's a complete loop for an agent to read the board, claim a task, work it, report what happened, and escalate what it can't decide. What's not in that list: nothing for goals — no lac_goal_add or lac_goal_plan equivalent. Nothing for the library — no document indexing, no lac_doc_symbol. Nothing for attestation or verification, and nothing for team administration. An agent sitting behind the MCP server today can work the queue and put a question to a human. It cannot start a new goal, catalogue a document, or attest a result — those still need the CLI, or a raw psql session against the same database.

Fails loudly, not silently

The server is deliberately unhelpful about guessing. No LAC_NAME returns "No identity set…" rather than inventing one. An unreachable database returns the actual connection error. And with no policy files reachable — say LAC_REPO wasn't set — lac_check_policy returns REQUIRE-APPROVAL, not "allow." It fails safe rather than silently waving everything through.

The security note that actually matters here

The DSN sitting in your MCP client's config is a live database credential, not a token you can revoke separately. Give that client a role with only the grants it needs — schema/grants.sql has the narrower roles — rather than a superuser connection. And know the actual boundary: anything holding the DSN can write to any lane regardless of what lac_check_permission reports back. That check, like cperm and crule on the CLI side, is cooperative — it constrains a client that asks first, not one that skips the question. The only thing that actually enforces a boundary at the database is team mode, where row-level security keys off the connected Postgres role rather than anything the client claims about itself.

Which one, when

Reach for the MCP server when the peer is an AI client that already speaks MCP and shouldn't need a shell — Claude Desktop, an editor's built-in agent, anything where installing CLIs on that machine is the wrong shape for how it runs. Reach for the CLI for everything that needs to keep running after the conversation ends: the keeper on cron, a worker on a headless box, team administration, goal planning, or a human watching lac board live. Nothing stops both from being peers on the same fleet at once — the desktop agent claiming research tasks over MCP while three lac work processes drain the backend lanes from a server, all four reading and writing the same lac_tasks table with no coordination layer between them beyond the one they're both already using.


How Lacunari works →  ·  Read the source