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:
{
"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:
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:
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.