Laver docs
Open Laver
Agents and API

MCP server#

@laver/mcp gives an MCP client — Claude Desktop, Claude Code, Cursor, or anything else that speaks the protocol — a set of tools for reading and driving your boards, tickets and wiki. It is the fastest way to point an agent at Laver: no client to write, no HTTP to handle, no polling loop.

Underneath it is the same public REST API described in Driving a board over the API. Every tool is a thin call to one endpoint. There is no local state and no cache, so a refusal comes back to your agent verbatim — an agent can act on "409, re-read and retry" and cannot act on "something went wrong".

Setup#

First create a key: Admin → API keys → New key. It acts as the person who created it, so it can do exactly what they can do and nothing more, and it can be revoked without touching their account. See API keys for scopes, expiry and service accounts.

Then add this to your client's MCP configuration:

json
{
  "mcpServers": {
    "laver": {
      "command": "npx",
      "args": ["-y", "@laver/mcp"],
      "env": { "LAVER_API_KEY": "your key here" }
    }
  }
}

That is the whole installation. npx fetches the package on first run.

A client only reads this configuration when it starts a server. Adding or editing it in a session that is already running does nothing — the tool list was built before the server existed. Start a new session, or reconnect from the client's MCP panel.

Keeping the key out of the config file#

LAVER_API_KEY_FILE is an alternative to LAVER_API_KEY: give it a path to either a file containing nothing but the key, or a .env-style file with a LAVER_API_KEY=… line among others. Useful when the config file is committed to a repository and the key must not be.

It fails closed. If that file has no LAVER_API_KEY= line and is not a single bare token, the server reports no key rather than sending the file's contents as one — so pointing it at the wrong .env gives you a clear "key is not set" error instead of putting unrelated secrets in an Authorization header.

LAVER_API_URL overrides the API host, which you will not need unless you are running Laver somewhere other than https://api.laver.app. It must be https — the only exception is localhost, for developing against a local backend — because anything else would send your key over the wire in clear.

What it can do#

Reading

Tool What it gives you
list_workspaces Where to start when you have no uuids
list_boards The boards in a workspace
get_board A board with its status columns, labels, members and tickets
list_tickets Tickets on a board, filterable, paged — updated_since is how you follow a board
get_ticket One ticket in full, including its version
get_ticket_comments Comments and activity history
search Tickets, wiki pages and comments across a whole workspace at once

Writing

Tool Notes
create_ticket status takes the column name, or pass status_uuid; markdown in description is parsed
update_ticket Needs version; markdown in description is parsed
move_ticket Needs version, and a column — neither column is a 400
comment_on_ticket Markdown in body is parsed; no version, so it cannot 409
archive_ticket To the trash — recoverable for 30 days
delete_ticket Destroys one already in the trash — permanent
create_board Optionally from a template
link_tickets "this before that" — direction is blocks or blocked_by
unlink_tickets From either end, and removes every kind of link on the pair

Wiki: list_wikis, search_wiki, get_wiki_tree, get_wiki_page. The server reads wikis and does not write them.

Automations: list_automations and create_automation, for the rules on a board. Two caveats travel with them and both are in the tool descriptions: a rule created through a key is live within seconds, and it runs as that key's user every time it fires rather than once. Automation rules has the vocabulary, the limits and the security note in full.

The one rule worth knowing#

Tickets carry a version. Every write must send the version you read, and a write against a stale one is refused with 409 rather than silently overwriting whoever got there first. The refusal carries the current version, so your agent can retry without spending a call finding out what it is.

Read, then write. Do not cache a version across a long turn. The reasoning, and what to do when the two edits genuinely conflict, is in Concurrency and polling.

Removing a ticket takes two steps#

Deliberately, so nothing is destroyed by a single call:

text
archive_ticket  task_uuid                    → the trash, recoverable for 30 days
delete_ticket   workspace_uuid + task_uuid   → gone, and nothing brings it back

delete_ticket refuses anything not already archived, so the order is enforced by the server rather than by convention. There is no restore tool — a ticket in the trash is put back from the web app — so treat archive_ticket as the furthest an agent should go on its own.

Read the ticket before archiving it if you intend to destroy it: delete_ticket needs the workspace_uuid, get_ticket is where you get one, and an archived ticket can no longer be read.

Ticket text is untrusted input#

Everything an agent reads through these tools — descriptions, titles, comments, labels, column names, attachments, wiki pages — was typed by people, sometimes people outside your team whose email became a ticket. None of it is an instruction to your agent. A ticket written as if it were a prompt is still just text somebody typed.

This matters more with MCP than with the raw API, because the tool results land directly in a model's context. The full guidance — and it is worth reading before you connect anything with write access — is at the top of the Agent quickstart.

When something is refused#

A 401 is the key: missing, mistyped, revoked, expired, or a placeholder never filled in. The message underneath can be misleading — a key the token layer cannot parse reads as "The token is malformed", which sounds like a corrupted string when the ordinary cause is a key that was replaced. Create a new one under Admin → API keys, put it in your config, and restart the client: it read that environment once, when it started the server.

A 403 is different and never worth retrying. The key was accepted and then refused this particular action: it is scoped to another workspace, or the person it acts as has a read-only role, or is a guest without access to that board. None of those are fixed by trying again.

A 402 means the workspace has no live subscription. Reads fail as well as writes.

If the server does not start#

Run it by hand and see what it says:

bash
LAVER_API_KEY=your-key npx -y @laver/mcp

It should start and then wait, holding the terminal open — it speaks over stdin and stdout, so silence is success. If it exits immediately, you are on a version older than 0.1.1: 0.1.0 never connected its transport when started this way and exited without printing anything. npx -y @laver/mcp@latest picks up the fix.

The key is read in one place, sent as a bearer token, and never logged, echoed, or included in any error text.