Lets an AI agent manage kanban boards, tickets, sprints, and workspace wikis via the Laver REST API.
Also known as Developyn/laver-mcp, @laver/mcp
- Stars
- 0
- Tools
- 49
- Downloads, 30 days
- —
- Latest version
- 0.5.0
- Last commit
- Aug 27, 2026
- License
- MIT
What is Laver MCP?
Laver lets an AI agent manage kanban boards, tickets, sprints, and workspace wikis via the Laver REST API. It exposes 49 tools, such as list_workspaces, list_boards, get_board.
Summary generated from the project's README
About Laver MCP server
Managing kanban boards, tickets, and workspace wikis is what developers can do through an AI agent using this server. It connects directly to the Laver REST API, providing tools to read and drive project management workflows without maintaining local state or a separate cache. Because every tool is a thin call to the public API, agents receive verbatim error responses, allowing them to react to specific issues like version conflicts.
To set up the server, you must provide a workspace-scoped API key via the LAVER_API_KEY environment variable or a file path. The server can be configured to use a custom host through the LAVER_API_URL setting. Users can perform a wide range of actions, from searching across an entire workspace with the search tool to managing complex board structures. It includes specific tools for reading ticket flows, managing subtasks, and organizing wiki trees, making it suitable for teams that want to automate triage, stand-ups, and documentation updates directly from their AI client.
What you can do with it
- 1Synchronizing ticket updatesA developer can instruct an agent to follow board changes by requesting only recently updated tickets.
Written from the project's README. Every use case is checked against a quote from it.
How to install Laver MCP
Formats verified 10 Nov 2026Install Laver MCP in Claude Code
$ claude mcp add --env LAVER_API_KEY=<YOUR_LAVER_API_KEY> laver-mcp -- npx -y @laver/mcpInstall Laver MCP in Cursor
~/.cursor/mcp.json
{
"mcpServers": {
"laver-mcp": {
"command": "npx",
"args": [
"-y",
"@laver/mcp"
],
"env": {
"LAVER_API_KEY": "<YOUR_LAVER_API_KEY>"
}
}
}
}Install Laver MCP in VS Code
.vscode/mcp.json
{
"servers": {
"laver-mcp": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"@laver/mcp"
],
"env": {
"LAVER_API_KEY": "<YOUR_LAVER_API_KEY>"
}
}
}
}Install Laver MCP in Claude Desktop
claude_desktop_config.json
{
"mcpServers": {
"laver-mcp": {
"command": "npx",
"args": [
"-y",
"@laver/mcp"
],
"env": {
"LAVER_API_KEY": "<YOUR_LAVER_API_KEY>"
}
}
}
}Install Laver MCP in Codex
~/.codex/config.toml
[mcp_servers.laver-mcp] command = "npx" args = ["-y", "@laver/mcp"] [mcp_servers.laver-mcp.env] LAVER_API_KEY = "<YOUR_LAVER_API_KEY>"
Install Laver MCP in Gemini CLI
~/.gemini/settings.json
{
"mcpServers": {
"laver-mcp": {
"command": "npx",
"args": [
"-y",
"@laver/mcp"
],
"env": {
"LAVER_API_KEY": "<YOUR_LAVER_API_KEY>"
}
}
}
}Laver MCP tools 49
Detected from the READMElist_workspaceslist_boardsget_boardlist_ticketslist_workspace_ticketsget_ticketget_ticket_commentsget_ticket_flowlist_custom_fieldslist_labelssearchcreate_ticket
| list_workspaces | — | |
| list_boards | — | |
| get_board | — | |
| list_tickets | — | |
| list_workspace_tickets | — | |
| get_ticket | — | |
| get_ticket_comments | — | |
| get_ticket_flow | — | |
| list_custom_fields | — | |
| list_labels | — | |
| search | — | |
| create_ticket | — |
Show all 49 toolsShow fewer tools
update_ticketmove_ticketcomment_on_ticketupdate_commentdelete_commentmark_comments_readadd_subtaskupdate_subtaskdelete_subtaskarchive_ticketdelete_ticketcreate_boardlink_ticketsunlink_ticketscreate_statusupdate_statusreorder_statusesdelete_statuscreate_groupreorder_groupsdelete_groupcreate_custom_fieldupdate_custom_fielddelete_custom_fieldcreate_labelupdate_labeldelete_labellist_ticket_attachmentsget_ticket_attachmentupload_ticket_attachmentdelete_ticket_attachmentlist_automationsget_automationlist_automation_runscreate_automationupdate_automationdelete_automation
| update_ticket | — | |
| move_ticket | — | |
| comment_on_ticket | — | |
| update_comment | — | |
| delete_comment | — | |
| mark_comments_read | — | |
| add_subtask | — | |
| update_subtask | — | |
| delete_subtask | — | |
| archive_ticket | — | |
| delete_ticket | — | |
| create_board | — | |
| link_tickets | — | |
| unlink_tickets | — | |
| create_status | — | |
| update_status | — | |
| reorder_statuses | — | |
| delete_status | — | |
| create_group | — | |
| reorder_groups | — | |
| delete_group | — | |
| create_custom_field | — | |
| update_custom_field | — | |
| delete_custom_field | — | |
| create_label | — | |
| update_label | — | |
| delete_label | — | |
| list_ticket_attachments | — | |
| get_ticket_attachment | — | |
| upload_ticket_attachment | — | |
| delete_ticket_attachment | — | |
| list_automations | — | |
| get_automation | — | |
| list_automation_runs | — | |
| create_automation | — | |
| update_automation | — | |
| delete_automation | — |
Configuration
LAVER_API_KEYRequiredSecretA workspace-scoped API key, created in Laver under Admin → API keys. It acts as the person who created it, so it holds exactly their access and can be revoked without touching their account. Supply this or LAVER_API_KEY_FILE.
LAVER_API_KEY_FILEAlternative to LAVER_API_KEY: a path to a file holding nothing but the key, or a .env-style file carrying a LAVER_API_KEY= line. Use this to keep the key out of your client's config file.
LAVER_API_URLOverrides the API host. Defaults to https://api.laver.app, and must be https except for localhost.
| Name | Description | |
|---|---|---|
| LAVER_API_KEY | A workspace-scoped API key, created in Laver under Admin → API keys. It acts as the person who created it, so it holds exactly their access and can be revoked without touching their account. Supply this or LAVER_API_KEY_FILE. | RequiredSecret |
| LAVER_API_KEY_FILE | Alternative to LAVER_API_KEY: a path to a file holding nothing but the key, or a .env-style file carrying a LAVER_API_KEY= line. Use this to keep the key out of your client's config file. | |
| LAVER_API_URL | Overrides the API host. Defaults to https://api.laver.app, and must be https except for localhost. |
Automated checks
Ran Nov 14, 2026Full report →Passed our automated checks. That is not a security audit: read the code and give the server the narrowest access it needs. How checks work
- Source repository linked and reachabledevelopyn/laver-mcp is public and reachablegithub.com/developyn/laver-mcp ↗Pass
- Package names this repository as its source@laver/mcp names developyn/laver-mcp as sourcenpmjs.com/package/@laver/mcp ↗Pass
- Build provenance (Sigstore attestation)No build attestation published for this releaseNot checked
- Open source license (OSI)MITspdx.org/licenses/MIT.html ↗Pass
- Commit or release in the last yearLast commit or release 2026-08-27github.com/developyn/laver-mcp ↗Pass
- OpenSSF ScorecardNot checked
- Package name not close to a popular packageNo popular package one edit away from @laver/mcpPass
- No critical advisory in the package or its dependencies99 resolved dependencies checked; no critical advisoryosv.dev ↗Pass
- No hidden instructions to the model49 tool descriptions and the README read; no override phrases, credential paths or hidden charactersPass
- Destructive tools are labelledNot checked
- Remote endpoint answers the MCP handshakeNot checked
README
View on GitHub@laver/mcp
An MCP server for Laver. Gives an agent tools to read and drive kanban boards, tickets and the workspace wiki.
Every tool is a thin call to the same public REST API the web app uses. There is no local state, no cache, and no second implementation of anything — if Laver refuses a write, the refusal comes back verbatim, because an agent can act on "409, re-read and retry" and cannot act on "something went wrong".
Setup
Create a workspace-scoped API key in Laver under Admin → API keys. 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.
{
"mcpServers": {
"laver": {
"command": "npx",
"args": ["-y", "@laver/mcp"],
"env": { "LAVER_API_KEY": "your key here" }
}
}
}
LAVER_API_URL overrides the API host; it defaults to https://api.laver.app.
LAVER_API_KEY_FILE is an alternative to LAVER_API_KEY: a path to either a
file containing nothing but the key, or a .env-style file with a
LAVER_API_KEY=… line among others (an assignment line wins; quotes and an
export prefix are both fine). That is how the .mcp.json in this repo
registers the server without a secret in a tracked file.
A file with neither — no assignment line, and more than one token in it — yields no key at all, and you get the "key is not set" error. It used to send the whole file as the token, which is fine for a file holding one secret and is a leak for anything else.
LAVER_API_URL must be https, except for localhost.
Working in this repo
.mcp.json at the repo root registers this server for anyone who opens the
project, reading the key from the gitignored .env. Nothing to export.
It runs the published package, npx -y @laver/mcp, rather than the
mcp/server.js beside it. That is deliberate: pointing it at the local file
meant everyone here ran the one code path no user takes, and that is precisely
how 0.1.0 shipped with an entry point that never connected its transport when
started through bin — which is the only way a real client starts it. Running
what we publish means we meet what users meet.
If you are editing this server, that same choice will fool you: your changes do nothing until they are published. Point the client at the working copy while you work on it —
{ "command": "node", "args": ["mcp/server.js"] }
— and put it back before you commit. npm run check and
frontend/tests/check-mcp-bin-entrypoint.mjs both run against the working copy
regardless, so the tests never depend on a publish.
A client only connects to MCP servers at startup. claude mcp add while a
session is already running does not retrofit the tools into that session — the
tool list was built before the server existed. Start a new session (or
reconnect from the client's MCP panel) and the tools appear.
Tools
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 — overdue, unassigned, or free text |
get_ticket |
One ticket in full, its subtasks, 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 field definitions — the uuids update_ticket writes against |
list_labels |
Every label in the workspace, not only the ones already used on one board |
search |
Boards, tickets, wiki pages and comments across a whole workspace at once |
list_workspace_tickets is the stand-up read: it needs no board_uuid, and it
is the only thing here that answers "what is late" and "what does nobody own"
without walking every board. It is deliberately the small sibling of
list_tickets — one page, 50 by default and 200 at most, no cursor — so narrow
it rather than paging it.
get_ticket_flow derives its numbers from the moves already in a ticket's
history. Read visits rather than the by_status totals if you are adding
several tickets up: tickets worked in one batch overlap, and their totals do not.
gaps says when the history and the ticket's current column disagree, which
makes the totals a floor rather than a measurement.
To follow a board, call list_tickets again with updated_since set to the
server_time the previous call returned; you get back the tickets that changed
and nothing else. There is no tool for the server-sent event stream at
GET /boards/:uuid/events — a tool call is one request and one answer, and a
stream that never ends is neither.
Asking for less. A tool result is charged against the model's context, and
three of these calls are the ones you cannot route around: a status_uuid or a
label_uuid comes from get_board, and a page_uuid comes from
get_wiki_tree. On a busy board they were 154 kB, 119 kB and 180 kB. Each now
takes a parameter that narrows the reply, and every one of them is opt-in — a
call that passes none of them is unchanged.
| Call | Gives you | Measured |
|---|---|---|
get_board(include_tasks: false) |
the structure alone — statuses, labels, fields | 155 kB → 17 kB |
get_wiki_tree(depth: 1) |
the top level, nothing nested under it | 119 kB → 1.8 kB |
get_wiki_tree(parent_page_uuid: …) |
one page and everything beneath it | 119 kB → 9.8 kB |
list_tickets(include_descriptions: false) |
titles and uuids without the bodies | 167 kB → 45 kB |
Reach for get_board(include_tasks: false) whenever you called it for a uuid
rather than for the tickets, and list_tickets when you want the tickets —
that one takes status and limit as well.
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 |
Your own comments only; replaces the whole body and is marked as edited |
delete_comment |
Your own comments only; to the trash, and nothing here restores one |
mark_comments_read |
Clears this ticket's unread badge for the user the key acts as |
add_subtask |
One checklist item, appended — plain text, not markdown |
update_subtask |
Tick it off (is_done), 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 — crm or sales-leads |
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 |
Comments are not versioned, so none of the three comment writes takes a
version and none of them can 409 — the last edit wins. Only the author may
edit or delete one, and somebody else's is a 404 indistinguishable from a
comment that does not exist, so these never report who wrote what.
get_ticket_comments deliberately marks nothing read; mark_comments_read is
the only call that does, and it marks up to the newest comment that exists at
that moment rather than subscribing.
Subtasks are the checklist on a ticket — a progress count on the card, and where
acceptance criteria belong when they are meant to be ticked off one at a time.
get_ticket returns the items themselves, so there is no separate list tool.
Board structure
| Tool | Notes |
|---|---|
create_status |
A new column, appended to the right-hand end |
update_status |
Rename, recolour, or set is_complete — a board has at most one |
reorder_statuses |
The complete list of uuids, in order; a stale list is a 409 |
delete_status |
The column must be empty, and a board keeps one |
create_group |
A swimlane; there is no rename route, so a wrong name is deleted and remade |
reorder_groups |
Same complete-list contract as reorder_statuses |
delete_group |
Tickets in it survive, ungrouped — and their versions all move |
create_custom_field |
Workspace owner or admin; type cannot be changed afterwards |
update_custom_field |
Rename, reorder, or replace a select's whole options list |
delete_custom_field |
Takes every ticket's value in that field with it — no trash, no restore |
This is what a board an agent creates needs to stop being its template's
defaults. The two reorder tools want the whole list because a partial one is how
two simultaneous reorders silently drop a column: read the uuids off get_board
immediately before calling, and re-read on a 409.
Labels
| Tool | Notes |
|---|---|
list_labels |
Workspace-wide, alphabetical — get_board shows only what a board uses |
create_label |
A hex colour is required; names are unique ignoring case and space |
update_label |
Renames it everywhere — one label, not a copy per board |
delete_label |
Removes it from every ticket that carries it |
A label is a workspace object shared by every board, which is the thing to be
sure of before renaming one: it changes for everybody. The uuids these return
are what update_ticket takes as label_uuids.
Attachments
| Tool | Notes |
|---|---|
list_ticket_attachments |
Name, type, size and uuid — never the bytes |
get_ticket_attachment |
Text inline, an image as an image block, anything else via save_to |
upload_ticket_attachment |
file_path for a file on disk, or text + filename for something written |
delete_ticket_attachment |
To the trash for 30 days; there is no restore tool |
get_ticket reports attachment_total, so you know whether listing is worth a
round trip.
Binary content crosses the tool boundary by not crossing it. A tool result
is text, Laver allows 25 MB per file, and 25 MB of base64 is roughly nine
million tokens — so only text, CSV and images under 4 MB come back inline, and
everything else needs save_to, which writes the file to a path on the machine
running this server (normally the agent's own, since the client starts it as a
subprocess). Uploads go the same way round: file_path reads from that machine
and costs no context.
An image comes back as an MCP image block rather than as text, which is the only form a model can actually look at — that is the whole point of the tool, since the screenshot somebody attached is usually the specification.
Wiki
list_wikis, search_wiki, get_wiki_tree, get_wiki_page,
get_wiki_page_version, append_wiki_page, update_wiki_page,
create_wiki_page — which takes the body as markdown and nests under
parent_page_uuid — and restore_wiki.
Archiving a wiki (done in the browser; there is no tool here for it) takes it
and every page under it out of list_wikis entirely. list_wikis takes
archived: true to see those instead — the only place an archived
wiki_uuid is visible at all — and restore_wiki is the only thing here
that does something with one: it un-archives the wiki, and every page under
it, in one call.
get_wiki_page_version is how you read something that was overwritten. It is a
read: the page does not move. There is deliberately no tool to put an old
version back — read it and write the wording you want with update_wiki_page,
which leaves a version of its own behind.
append_wiki_page adds to the end of a page and update_wiki_page replaces
what is on one; prefer the first when you are adding, because 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 somebody wrote to meanwhile is a
409 carrying the current version rather than an overwrite — the guard that lets
this exist at all, since wiki pages sit behind a live collaborative editor. A
replace re-seeds that editor from what it just wrote, so a colleague with the
page open sees the new text instead of putting the old text back, and every
previous save stays readable through get_wiki_page_version. There is still no
tool to delete a page.
The markdown goes through the same parser ticket descriptions do. Headings,
lists, tables, code blocks, blockquotes, rules and links survive; so does an
 image, as a reference to that URL — there is no tool here to
upload an attachment, so the URL has to be public already. Raw HTML is kept as
literal text rather than interpreted.
Automations
| Tool | Notes |
|---|---|
list_automations |
The rules on a board, each with its version |
get_automation |
One rule in full — the conditions and actions an edit replaces |
list_automation_runs |
What a rule has actually done, newest first |
create_automation |
Owner or admin only — and see below before calling it |
update_automation |
Needs version; enabled: false is the reversible stop |
delete_automation |
Needs version, and takes the rule's whole run history with it |
An automation rule is a trigger, optional conditions and up to twenty actions, stored against a board. Three things about them are worth knowing before an agent touches these tools:
- A rule created here is live immediately. It fires on its trigger within a
couple of seconds, so read
list_automation_runsafterwards rather than creating one speculatively to see what it would do. - A rule is a standing grant. It runs as the user this key acts as, every time it is triggered, for as long as it exists — not once, like every other write in this server. Revoking the key does not stop it; disabling or deleting the rule does.
- Pause before you delete.
update_automationwithenabled: falsestops a misbehaving rule at once and keeps its history; deleting destroys the history along with the rule.conditionsandactionsreplace the stored lists rather than merging into them, so build them fromget_automationand not from memory.
Both writes take the rule's version and answer 409 with the current one, the
same contract ticket writes have.
Published links
list_published_links is the inventory read: what of this workspace's is on the
public internet right now, each row with its share token, who published it, when,
and how many strangers have looked. live is what a stranger actually gets and
dark_reason says why a row is not, which is the only way to find a published
page that is currently archived and would go straight back online if somebody
restored it. Workspace owner or admin only; an ordinary member is a 403. Taking
a link back down is deliberately not a tool — report what is published and let a
person decide what comes off.
Laver MCP questions
How do I add Laver MCP to Claude Code?
Run claude mcp add --env LAVER_API_KEY=<YOUR_LAVER_API_KEY> laver-mcp -- npx -y @laver/mcp in your terminal. Add --scope user to make it available in every project.
How do I add Laver MCP to Cursor?
Open ~/.cursor/mcp.json and add the server under mcpServers, using the Cursor config shown in the install section above.
What tools does Laver MCP provide?
It exposes 49 tools, including list_workspaces, list_boards, get_board, list_tickets, list_workspace_tickets.
Does the server maintain a local cache?
No, every tool is a direct call to the public REST API, meaning there is no local state or cache.