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. Where
that configuration lives, and how each client reloads it, differs per client —
MCP setup by client walks Claude Code,
Claude Desktop and Cursor through it step by step.
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 |
list_workspace_tickets |
Triage across every board at once — what is overdue, what nobody owns |
get_ticket |
One ticket in full, its checklist, including its version |
get_ticket_comments |
Comments and activity history |
get_ticket_flow |
How long the ticket has spent in each column |
list_custom_fields |
A board's custom fields — the uuids update_ticket writes values against |
list_labels |
Every label in the workspace, not only the ones a board already uses |
search |
Boards, tickets, wiki pages and comments across a whole workspace at once |
list_workspace_tickets is the stand-up read: no board_uuid, and the only
thing here that answers "what is late" and "what does nobody own" without
walking every board. get_ticket_flow derives its numbers from the moves
already in a ticket's history — read its visits rather than its per-column
totals if you are adding several tickets up, because tickets worked in one batch
overlap and their totals do not.
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; custom_fields is keyed by field uuid and replaces the whole object |
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 |
update_comment |
Its own comments only — it replaces the body, and the comment is marked edited |
delete_comment |
Its own comments only, to the trash; nothing here puts one back |
mark_comments_read |
Clears the ticket's unread badge for the person the key acts as |
add_subtask |
One checklist item, appended — plain text, and one call each |
update_subtask |
Tick it off, rename it, or move it up the list |
delete_subtask |
Not recoverable — a checklist item has no trash |
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 |
Only the author can edit or delete a comment, and reading a thread never marks
it read — mark_comments_read is the one call that does, which is how an agent
clears the badge its own comment created. Subtasks are the checklist on a card,
and they are where acceptance criteria belong when they are meant to be ticked
off one at a time rather than read as prose.
Board structure: create_status, update_status, reorder_statuses and
delete_status for the columns; create_group, reorder_groups and
delete_group for the swimlanes; list_custom_fields, create_custom_field,
update_custom_field and delete_custom_field for the fields. A board an agent
creates no longer has to keep its template's defaults. The two reorder tools
want the complete list of uuids and refuse a stale one with a 409, so nobody's
simultaneous reorder quietly drops a column. Deleting a column needs it empty
first; deleting a custom field takes every ticket's value in that field with it,
and nothing brings those back.
Labels: list_labels, create_label, update_label and delete_label. A
label belongs to the workspace rather than to a board, so renaming one changes
it everywhere at once — get_board shows only the labels a board already uses,
and list_labels is where the rest are.
Attachments: list_ticket_attachments, get_ticket_attachment,
upload_ticket_attachment and delete_ticket_attachment — so an agent can read
the screenshot or the spec somebody attached, and can attach its own evidence
rather than describing it. get_ticket reports attachment_total, so it knows
whether there is anything to fetch.
Binary content crosses the tool boundary by mostly not crossing it. A tool
result is text, files can be 25 MB, and 25 MB of base64 is roughly nine million
tokens — so text and CSV come back inline, an image comes back as an image block
the model can actually look at, and anything else is written to a path you give
as save_to, on the machine running the server. Uploads work the same way
round: file_path reads from that machine and costs no context, while text
plus filename is there for something the agent has just written.
Wiki: list_wikis, search_wiki, get_wiki_tree, get_wiki_page,
get_wiki_page_version, append_wiki_page, update_wiki_page,
create_wiki_page, and
restore_wiki. get_wiki_page_version reads what a page said at an earlier
save — the way to recover something that was overwritten, and a read, so the
live page does not move. create_wiki_page takes the page body as markdown,
so an agent that has worked something out can write it up where it will be
found rather than handing it back as chat. append_wiki_page adds to the end
of a page that exists and update_wiki_page replaces what is on one; reach for
the first when you are adding, since it takes no version and two agents
appending at once both get their text. Replacing takes the version from
get_wiki_page, and a page written to meanwhile is a 409 carrying the current
version rather than an overwrite — the guard that makes it safe to offer,
because wiki pages have a live collaborative editor behind them. A replace
re-seeds that editor from what it wrote, so a colleague with the page open sees
the new text rather than putting the old text back, and every earlier save
stays readable through get_wiki_page_version. There is no tool to delete a
page. Both reads hand the body back as content_markdown — the same markdown
the three writes take — so correcting a page is: read it, edit the text, send it
back. Beside it, markdown_omits names anything on the page markdown cannot
carry, such as a mention of a person or a signature blank; it is normally empty,
and when it is not, append rather than replace. The markdown goes through the same
parser ticket descriptions do: headings, lists, tables, code blocks and links
survive, an  image survives as a reference to that URL —
there is no tool here to upload an attachment, so it has to be public already
— and raw HTML is kept as literal text rather than interpreted. Archiving a
wiki takes it, and every page under it, out of list_wikis entirely; pass
archived: true to see those instead, and restore_wiki is the only thing
here that acts on one — it un-archives the wiki and every page under it.
Automations: list_automations, get_automation, list_automation_runs,
create_automation, update_automation and delete_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. update_automation with
enabled: false is the reversible way to stop a rule that is misbehaving —
deleting it destroys its run history too — and both writes take the rule's
version and answer 409 with the current one, exactly as ticket writes do.
Automation rules has the vocabulary, the
limits and the security note in full.
Published links: list_published_links answers "what of ours is on the
public internet right now" — every published board and wiki page, its share
link, who published it, how many strangers have looked, and whether it is
actually reachable. Workspace owners and admins only. Taking a link back down is
deliberately not a tool: an agent reports the inventory and a person decides.
What the MCP server does not cover#
The REST API is larger than the MCP server, and the difference is deliberate rather than accidental. Every backend route is listed in the repo against either the tool that calls it or the reason it has none, and the server's own check fails if a route turns up in neither — so this list cannot quietly go stale the way it did before.
Not yet, and wanted:
- Notifications — still the biggest gap, and now waiting on a decision
rather than on delivery. An agent cannot see that it was mentioned or
assigned, and the notification routes accept a signed-in session only, so an
API key is refused there however good the tool is. Whether a key may read its
owner's inbox — a surface carrying other people's messages — is the question
to settle first.
list_workspace_ticketscovers the triage half meanwhile. - Listing subtasks on their own — needs no tool rather than lacking one.
get_ticketalready returns the checklist items themselves, and writing them is covered. - Board templates — a board you saved as a template cannot be listed, saved
or deleted from here, and
create_boardaccepts the built-in ids only, so a new board seeded from one of your own templates is made in the app. - Attachment thumbnails — deliberate rather than missing. The downscaled
copies the web app draws card covers from are no use to an agent, which wants
the uploaded file itself;
get_ticket_attachmentalready returns that. - Where you left off — the recent tickets and pages the ⌘K palette shows a
person before they type. It is built from what somebody opened in a browser,
and work done through a key is excluded from it deliberately, so there is
nothing here an agent would want that
searchandlist_workspace_ticketsdo not answer better. - Board analytics — a smaller loss now that triage is covered. What is left
is shaped for charts rather than for a decision: board statistics, the
activity feed, and the board export.
get_ticket_flowcovers the one figure an agent acts on, per ticket. - The wiki page a board is about — deliberate, because the pieces are
already here.
get_boardreturns the board'swiki_page_uuid, andget_wiki_pageandget_wiki_treeread that page and the pages under it. The route is a shortcut for a person looking at the board, not a fact only it holds. - A conductor's scoreboard — never. It reads back how one person's tickets fared after an agent picked them up, for that person only. An agent reading the scores of the people who brief it is the wrong way round, and could play to them.
- Pressing an automation button — deliberate, not pending. A
manualrule exists so a person decides when it runs; creating one is covered, firing one is not. - Ticket history, duplication and recurrence.
- Sprints — blocked where notifications are: the sprint routes accept a signed-in session only, so a key is refused. What a key may do to a team's planning cadence is the decision to take first — a rollover moves every unfinished ticket onto a new board.
- Imports and feedback forms.
- Deleting and rearranging a wiki page — editing one is covered now, by
update_wiki_page, under the version check that answers the conflict story it waited on. Archiving, moving and duplicating are not: they change what a colleague can find rather than what a page says, and a page nobody can find has no version history to fall back on. Removing a whole wiki sits in the same group:DELETE /wikis/:wiki_uuidarchives the wiki and every page beneath it, and takes any published pages off the internet in the same instant — consent, not editing. Its restore route is covered instead, byrestore_wiki. - Commenting on a wiki page — left out because of what a comment is
attached to. A comment marks the words it is about, and the app finds those
words again in the page it has just drawn; an agent has no page in front of
it, so a phrase that appears twice would quietly pin the remark to the wrong
sentence.
append_wiki_pageandupdate_wiki_pageare the honest way for an agent to say something about a page — in the page, where you will see it. Editing, withdrawing and resolving a comment follow from that, and resolving one closes a conversation between people.
Not ever, from an API key:
- Reporting a deployment. Telling Laver that a ticket's pull request reached an environment, or failed to, is a report from your build pipeline — a CI job with a key in a secret, running when no agent is. A tool over it would let an assistant announce a deployment nothing performed, and the worth of that notification is that it states a fact. What an agent might want to read it already has: each deployment lands on the ticket's timeline.
- Reporting a merged pull request. Same again, plus one: the report moves
the ticket into the column tagged for merged pull requests, and an agent that
wants to move a ticket has
move_ticketfor it. The difference a tool would add is the sentence on the timeline saying the pull request was merged, which only the pipeline that watched it merge can honestly say. Agents can read it: the merge lands on the ticket's timeline like a deployment does. - Public links and publishing. Making something public is a decision a person takes with the consequence in front of them.
- A board's GitHub webhook. The screen that shows it hands back a live
credential — the URL your repository posts to and the secret that signs each
delivery — and there is nothing an agent needs it for: the webhook is how
GitHub tells a board a pull request merged, and an agent that wants to move a
ticket has
move_ticket. Rotating it is the same, from the other side, since it stops the URL already in your repository's settings from working and a person has to paste in the new one. - Taking a published link back down. The same split, from the other side:
list_published_linksanswers what is public and grants no access a reader did not already have, since every link it names is public by definition. Retracting one is a button pressed in front of a screen showing what is about to go dark, so an agent reports and a person decides. - Asking a person for their signature. Sending a signature request puts a
message into a stranger's inbox with Laver's name on it, and deciding that an
address deserves to be asked is a person's judgement — so all three routes
refuse a key outright rather than leaving it to permissions. Reading the
outstanding requests needs no tool either:
get_wiki_pagealready carries the page an agent wants. - Taking a board away, and how it looks. Archiving and deleting a board are
one write under two names, and either would take a whole board and every
ticket on it out of view in one call. Permanent deletion is firmer still and
is confirmed twice, in the app, from a screen showing what is about to be
lost. A board's colour and background picture are decoration somebody chooses
while looking at the board. A column's entry requirements are here too: the
read needs no tool, because
get_boardalready carries them and a refusedmove_ticketnames every requirement the ticket does not meet — but a key that could set them could remove them and then move the ticket. - Moving a board between workspaces. A key is confined to the one workspace it was issued for, and moving a board names two of them: where it is, and where it is going. There is no key that reaches both, so there is no tool to write. It is an admin's decision anyway — which of the two workspaces they administer a whole board, with its tickets, comments and history, belongs to — and the app asks for admin at both ends before it moves anything.
- Workspace and membership administration. A key acts as the person who created it, and renaming or deleting their workspace reaches further than delegating a board task ever meant.
- The whole-workspace export. One file holding every ticket, comment and wiki page you have. An owner or admin asks for one in the app, signed in, and the audit log records who; a key that could ask for it would turn any leak of that key into a copy of everything.
- Bulk ticket writes. Archiving twenty tickets on one mistaken argument is a blast radius no error message makes safe.
- Trash. One-way by design: an agent archives and can destroy what it archived; a person restores.
- Hiding a ticket from its board. Hiding takes a card off the board without
taking the work out of the board's numbers, and it is a tidy-up somebody does
while looking at a column — a view an agent does not have.
get_boardandlist_ticketsboth filter hidden tickets out, so a key that hid one would then read a board it is not on, and the read that lists them again is withheld for the same reason.archive_ticketcovers a ticket nobody means to look at again. - The board event stream. Server-sent events, where a tool call is one
request and one answer.
list_ticketswithupdated_sincereplaces it. - Scheduler endpoints, which are the deployment's own cron hooks.
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 is billing, and it never stops a read. Either the workspace has hit a plan limit, or an invoice has gone unpaid and it is read-only until it is settled. Tools that only read carry on; tools that write are the ones refused.
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.