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. 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 ![alt](https://…) 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_tickets covers the triage half meanwhile.
  • Listing subtasks on their own — needs no tool rather than lacking one. get_ticket already 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_board accepts 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_attachment already 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 search and list_workspace_tickets do 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_flow covers the one figure an agent acts on, per ticket.
  • The wiki page a board is about — deliberate, because the pieces are already here. get_board returns the board's wiki_page_uuid, and get_wiki_page and get_wiki_tree read 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 manual rule 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_uuid archives 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, by restore_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_page and update_wiki_page are 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_ticket for 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_links answers 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_page already 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_board already carries them and a refused move_ticket names 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_board and list_tickets both 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_ticket covers 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_tickets with updated_since replaces 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:

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 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:

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.