# io.github.Developyn/laver-mcp (npm · @laver/mcp)

Kanban boards, sprints, tickets and a team wiki, with versioned writes for concurrent agents.

- Trust score: 68/100 (medium)
- Registry status: active
- Liveness: live
- Owner verified: no
- Last scored: 2026-08-18

## Components

- npm · `@laver/mcp`: 68/100 (this document), [markdown](https://verifymcp.io/servers/developyn-laver-mcp/laver-mcp.md), [page](https://verifymcp.io/servers/developyn-laver-mcp/laver-mcp)

## Channel facts

- Registry: `npm`
- Package: `@laver/mcp`
- Version: `0.4.0`
- Transport: `stdio`

## Trust breakdown

How this component scores in each security and reliability category. Every signal is checked automatically from public evidence about the published package, including repeated runs of it in an isolated sandbox, and we only credit what we can confirm. Scores are 0–100 per category. Scoring method: https://verifymcp.io/docs/scoring (what has changed: https://verifymcp.io/docs/scoring/changelog)

Scored 2026-08-18.

- **Supply Chain Security**: 98/100
  - No malware found by supply-chain analysis.
  - No known CVEs affecting this package version or its production dependencies.
  - No install/post-install scripts declared.
  - 30 of 95 dependencies flagged as unhealthy.
- **Provenance & Transparency**: 45/100
  - Source repository is publicly reachable at the declared URL.
  - Provenance check failed: no build-provenance attestation is published.
  - Clear OSI-approved license (MIT).
  - Actively maintained (last published 2 days ago).
  - Disclosure check failed: no security disclosure policy was found in the source repository.
- **Schema Quality & AI Usability**: 65/100
  - AI-judged instruction clarity (excellent).
  - Context-footprint check failed: tool/resource definitions use about 16889 tokens (~291/item across 58 items; 58 tools + 0 resources), over budget; trim descriptions and params.
  - Usage-examples check failed: none of the tools include examples.
- **Stability & Change Management**: 13/100
  - Stability observed for 4 of 30 days with no destabilising changes; credit accrues until the full window elapses.
- **Tool Coverage**: 100/100
  - 100% of tools have a non-trivial description (not blank, and not just the tool's name).
  - 100% of tool parameters carry a description.
- **Capabilities**: 100/100
  - Implements a supported MCP spec version (2025-11-25); the latest is 2026-07-28.

## Install

### Claude

```bash
claude mcp add developyn-laver-mcp -- npx -y @laver/mcp
```

### Codex

```bash
codex mcp add developyn-laver-mcp -- npx -y @laver/mcp
```

### opencode

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "developyn-laver-mcp": {
      "type": "local",
      "command": [
        "npx",
        "-y",
        "@laver/mcp"
      ],
      "enabled": true
    }
  }
}
```

### OpenClaw

```bash
openclaw mcp add developyn-laver-mcp --command npx --arg -y --arg @laver/mcp
```

### Hermes

```yaml
mcp_servers:
  developyn-laver-mcp:
    command: "npx"
    args: ["-y", "@laver/mcp"]
```

### Other

```json
{
  "mcpServers": {
    "developyn-laver-mcp": {
      "command": "npx",
      "args": [
        "-y",
        "@laver/mcp"
      ]
    }
  }
}
```

## Changelog

Every change recorded for this component, newest first. Days that predate change tracking, or that we cannot explain, say so: "we were watching and nothing happened" and "we were not watching" are different claims.

### 2026-08-17 (score 68, +1)

No change was recorded against any check on this day. Stability & Change Management went from 7 to 10. That category is still filling its 30-day observation window: 2 days of observed history at the previous scan, 3 at this one. The score rises as the window fills, whether or not the server changes.

### 2026-08-16 (score 67, +2)

- [functional regression] Schema quality: 8991 → 16889
- [functional improvement] Tool coverage: 71% → 100%
- [functional improvement] Stability: unverified → 0.07
- [functional] Package version: 0.3.1 → 0.4.0

### 2026-08-14 (score 65)

First indexed and scored.

## MCP tools (58)

### `list_workspaces` (~140 tokens)

The workspaces this key can act in, with their uuids, names and the role it holds in each. Start here when you do not already have a workspace uuid, then go to list_boards. Through an API key this is always exactly one workspace — the one the key was issued for — even when the person who created it belongs to several, so a single entry is the normal answer rather than a sign of missing access. Archived workspaces are not in it. Each entry carries `billing_status`, which is how you tell a workspace that has gone read-only from one you can still write to, before a write fails rather than after.

### `list_boards` (~144 tokens)

The boards in a workspace, with their uuids and names — where you go from a workspace to something you can actually read. It lists only the boards this key can open, so a short list means limited access rather than an empty workspace, and archived boards are not in it. The uuid you want for get_board and list_tickets is here; the status uuids those need are not — get_board has those.

Input parameters:

- `workspace_uuid` (string, required): The workspace's uuid, from list_workspaces. A key only ever reaches the one workspace it was issued for, so this is that one; another workspace's uuid is a 404 rather than a permission error.

### `get_board` (~266 tokens)

A board with its status columns, labels, members and tickets. The status uuids returned here are what create_ticket and move_ticket expect. Each ticket carries `label_uuids` — bare uuids, resolvable against the board's `labels` list in the same reply — which is exactly the field `update_ticket` takes, so a label read from here can be written straight back without remapping. Note that `list_tickets` and `get_ticket` still return expanded `labels` objects, because neither reply has the dictionary to resolve uuids against. The tickets are around 90% of this reply, so pass `include_tasks: false` when you came for the uuids.

Input parameters:

- `board_uuid` (string, required): The board's uuid, from list_boards
- `include_tasks` (boolean): Set false to leave the tickets out and get the board's structure alone — statuses, labels, members, custom fields and `server_time`. That is what you want when you called this for a status_uuid or a…

### `list_tickets` (~825 tokens)

Tickets on a board, in board order, optionally filtered by status or by free text. This is what to read a column or walk a whole board with; get_board returns the same tickets but takes neither `limit` nor a cursor. Paging: `limit` defaults to 50 and caps at 200, and `next_cursor` comes back only on a FULL page — a short page is the end of the walk, so stop when it is absent instead of calling again, and pass it back verbatim when it is there. Following a board over time: send the `server_time` from a previous reply as `updated_since` and you get only what changed since, plus `removed_task_uuids` for tickets that left the board or went out of view. Use the server's clock for that rather than your own, which is the whole point of `server_time` — your clock can drift and silently skip a ticket. Archived tickets are never returned.

Input parameters:

- `board_uuid` (string, required): The board to read, from list_boards
- `cursor` (string): The `next_cursor` from the previous page, passed back unchanged. It encodes a position on the board, so it is only meaningful for the same board and the same filters — do not build one yourself or re…
- `include_descriptions` (boolean): Set false to leave `description` off every ticket, keeping titles, statuses, labels and uuids. Bodies are around 78% of a column listing, and you have not chosen a ticket to read yet — get_ticket has…
- `limit` (integer): Tickets per page, 1 to 200. Defaults to 50. A reply holding exactly this many is a full page and carries a `next_cursor`; anything shorter is the last page.
- `q` (string): Free-text search over titles and descriptions, ranked by relevance. Whole words and prefixes, not mid-word substrings. A ranked reply cannot be paged, so this cannot be combined with cursor. A UUID,…
- `status` (string): A status column's name, as shown on the board — 'To Do', 'In progress'. Matched against that board's columns, so a name from another board is a 400 rather than an empty list. Use status_uuid instead…
- `status_uuid` (string): A status column's uuid, from get_board. The exact form of `status`, and the one to prefer once you have read the board, since it survives a column being renamed.
- `updated_since` (string): Return only tickets changed after this moment: an ISO 8601 timestamp, or — better — the `server_time` from a previous reply, which is the server's own clock and cannot drift against it. The reply the…

### `search` (~352 tokens)

Search tickets, wiki pages AND ticket comments across a whole workspace in one call, ranked by relevance with a highlighted excerpt. Use this when you know roughly what something is called but not which board or wiki it is on — list_tickets needs a board_uuid, this does not. The reply has FOUR lists: `boards` and `tasks` and `pages` matched their own text, and `comments` are tickets found by something said ABOUT them — each carries the ticket it belongs to, and a ticket already in `tasks` is never repeated there. `boards` is easy to miss and is often the one you want: it is how you find the board a term names without listing every board first. Matches whole words and prefixes, not mid-word substrings: 'deploy' finds 'deployment', 'eploy' finds nothing. Three characters minimum. Returns a short line per hit, not the full ticket or comment — follow up with get_ticket, get_ticket_comments or get_wiki_page. Each excerpt marks the matched words with the control characters \x01 and \x02 rather than with markup; strip them before showing the text to anyone.

Input parameters:

- `limit` (integer): Results per kind, tickets and pages counted separately
- `q` (string, required): What to look for, 1 to 200 characters. Whole words and prefixes across every board, ticket, comment and wiki page the key can open — three characters minimum for a prefix to count.
- `workspace_uuid` (string, required): The workspace's uuid, from list_workspaces. A key only ever reaches the one workspace it was issued for, so this is that one; another workspace's uuid is a 404 rather than a permission error.

### `list_workspace_tickets` (~502 tokens)

Tickets from EVERY board in the workspace at once — the triage read, and the only one here that does not need a board_uuid. Two questions it answers that nothing else can: `overdue: true` is everything past its due date and not yet in a column the board counts as finished, oldest deadline first; `unassigned: true` is everything nobody owns. Send both and you get the intersection, since the filters are ANDed rather than ORed. `q` is the same word-and-prefix search list_tickets uses, ranked, across the whole workspace. Deliberately the small sibling of list_tickets: one page, 50 by default and 200 at most, no cursor and no paging — so a `limit`-sized reply means there may be more and you cannot ask for the rest from here. Narrow it, or go board by board with list_tickets. It shows only the boards this key can open, so a restricted member's triage is their own boards and the reply never says which ones were left out. Archived tickets are not in it.

Input parameters:

- `limit` (integer): How many tickets to return, 1 to 200. Defaults to 50. There is no cursor on this route, so this is a ceiling rather than a page size — a full reply is not proof there is nothing else.
- `overdue` (boolean): true for tickets whose due date has passed and that are not in a column marked complete. The comparison uses the server's calendar day rather than yours, so the same ticket is overdue for everybody a…
- `q` (string): Free-text over titles and descriptions across every board, ranked by relevance. Whole words and prefixes, not mid-word substrings, and three characters minimum for a prefix to count. Unlike list_tick…
- `unassigned` (boolean): true for tickets with nobody assigned. Combine with `overdue` for the pair of questions triage usually asks. Omit or false for no filter.
- `workspace_uuid` (string, required): The workspace's uuid, from list_workspaces. A key only ever reaches the one workspace it was issued for, so this is that one; another workspace's uuid is a 404 rather than a permission error.

### `get_ticket` (~200 tokens)

One ticket in full: its fields, its `subtasks`, its expanded `labels` objects, and the `version` every write needs. Read this before you write. update_ticket, move_ticket, archive_ticket and the rest all take that version and answer 409 if it has moved on since you read it — that 409 is the signal to call this again and retry against the fresh version, never to drop the version or force the write. `labels` arrive expanded rather than as bare uuids because this reply carries no board dictionary to resolve them against; get_board returns `label_uuids` instead, and that bare-uuid shape is the one update_ticket wants. A ticket you cannot open answers 404, the same 404 as one that never existed, so a 404 here is not proof the ticket is gone.

Input parameters:

- `task_uuid` (string, required): The ticket's uuid, from list_tickets, get_board or search

### `get_ticket_comments` (~163 tokens)

The discussion on a ticket, as two lists: `comments` — the thread, oldest first — and `events`, the activity history the web app draws alongside it (moves, field edits, who did what and when). The whole thread comes back in one call; there is no paging and no cursor, so a very long one is simply long. Reading is free of side effects: it does NOT mark the thread as read or clear anyone's unread badge, so polling this to follow a ticket cannot make a teammate think their comment has been seen. A ticket you cannot open is a 404, the same 404 a ticket that never existed gives.

Input parameters:

- `task_uuid` (string, required): The ticket's uuid, from list_tickets, get_board or search

### `get_ticket_flow` (~292 tokens)

How long this ticket has spent in each column, derived from the moves already recorded in its history — the answer to "where is the time going". READ `visits` RATHER THAN THE `by_status` TOTALS if you are adding several tickets up: each visit is one stay in one column with its own `entered_at`, `left_at` and `hours`, where `by_status` is those visits already summed per column — and a ticket's per-column totals overlap with another's whenever the two were worked in the same batch, so summing totals across tickets double-counts wall-clock time that was shared. The hours are wall clock, not working hours: a ticket that sat over a weekend counts the weekend. `gaps` is the honest part of the reply and is worth reading before quoting any number — a ticket created before its board recorded moves has `no_move_recorded`, and `chain_broken` or `status_mismatch` mean the history and the ticket's current column disagree, so the totals are a floor rather than a measurement. The current column's visit is still open, with `left_at` null, and its hours grow until the ticket moves. A ticket you cannot open is a 404, the same 404 one that never existed gives.

Input parameters:

- `task_uuid` (string, required): The ticket's uuid, from list_tickets, get_board or search

### `list_ticket_attachments` (~148 tokens)

The files on a ticket: name, content type, size in bytes and uuid, newest first. Nothing else here reads a ticket's files, so this is the way to find out whether the specification an agent is working from is actually a PDF somebody attached. `get_ticket` reports `attachment_total`, which is how you know whether calling this is worth a round trip. Only finished uploads are listed — an upload still in flight and a deleted file both read as absent — and the reply carries metadata, never the bytes; get_ticket_attachment fetches those one at a time.

Input parameters:

- `task_uuid` (string, required): The ticket's uuid, from list_tickets, get_board or search

### `get_ticket_attachment` (~291 tokens)

Fetch one attachment's CONTENT, having found its uuid with list_ticket_attachments. What comes back depends on what the file is, because a tool result is text and most files are not: a text or CSV attachment is returned inline as text; an image is returned as an image block, which is the only form the model can actually look at; anything else — a PDF, a spreadsheet, a document — cannot cross this boundary as text at all, and needs `save_to`. Pass `save_to` for any file you want on disk, and for anything large: it writes the bytes to that path on the machine THIS SERVER runs on (normally the same machine as the agent, since the client starts it as a subprocess) and returns the path and size instead of the content. Files over 4 MB always need `save_to`, whatever their type, because an inline result that size costs more context than the answer is worth. Missing, still uploading, deleted, or on a ticket you cannot open are all the same 404.

Input parameters:

- `attachment_uuid` (string, required): The attachment's uuid, from list_ticket_attachments
- `save_to` (string): Absolute path to write the file to, on the machine running this server. Parent directories are created. An existing file is overwritten
- `task_uuid` (string, required): The ticket's uuid, from list_tickets, get_board or search

### `create_ticket` (~476 tokens)

Create a ticket on a board. Give either `status` (the column name) or `status_uuid`; with neither it lands in the first column. Markdown in `description` is parsed — headings, lists and code fences all render. It is appended to the bottom of its column unless you send a `position`. The reply is the new ticket, including the `uuid` to reference it by and the `version` any later write to it will need, so there is no need to re-read it before your next call. This tool sends no idempotency key, so calling it twice makes two tickets: on a timeout or an unclear failure, use list_tickets with `q` set to the title to see whether the first one landed before you retry.

Input parameters:

- `board_uuid` (string, required): The board to create it on, from list_boards
- `description` (string): The body, Markdown, up to 20000 characters. Headings, lists and code fences all render.
- `due_date` (string): Due date as an ISO 8601 date, e.g. 2026-08-14
- `position` (number): Sort key within the column, not an index — smaller sorts higher. Read the neighbours' `position` off get_board and send a number between them; omit it to append to the bottom. Positions come back as…
- `priority` (string): One of low, medium, high or urgent. Left off, the ticket simply has no priority set, which is not the same as low.
- `status` (string): The column to place it in, by name as shown on the board. Must be a column on this board. Send this or `status_uuid`, not both; with neither it lands in the first column.
- `status_uuid` (string): The column to place it in, by uuid, from get_board. The exact form of `status` and the one to prefer once you have read the board, since renaming a column does not break it.
- `title` (string, required): The ticket's title, 1 to 500 characters. This is the whole of what shows on the card, so put the specifics here rather than in the description alone.

### `update_ticket` (~814 tokens)

Change a ticket. `version` must be the one from get_ticket; a 409 means somebody wrote first and you should re-read. Markdown in `description` is parsed, and it replaces the whole description rather than appending to it. `label_uuids`, `assignee_uuids` and `custom_fields` each REPLACE the whole set rather than adding to it, so send what is already on the ticket alongside what you are adding or you will silently remove the rest. `custom_fields` is keyed by the field's uuid — read them off get_ticket, since a name will not do — and a uuid no field on that board has is DROPPED SILENTLY rather than refused: the call answers 200 and the value is simply not there. The task in the reply carries the stored `custom_fields`, so read them back to confirm a write landed rather than assuming a 200 means it did.

Input parameters:

- `assignee_uuids` (array): The ticket's assignees, as the complete set after the write — it REPLACES rather than adds, so include whoever is already assigned. The uuids come from the board's `members`. An empty array unassigns…
- `custom_fields` (object): Custom field values, keyed by the FIELD's uuid — read them off get_ticket, a field's name will not work. REPLACES the whole set, so send the values already on the ticket alongside the ones you are ch…
- `description` (string): Replaces the whole body, Markdown, up to 20000 characters — it does not append. To add a line, read the current description with get_ticket and send it back with your addition included.
- `due_date`: Due date as an ISO 8601 date, e.g. 2026-08-14. Explicit null clears it; omitting it leaves it unchanged. Those are different, so do not send null to mean 'no change'.
- `label_uuids` (array): The ticket's labels, as the complete set after the write — it REPLACES rather than adds, so include the ones already on the ticket or you remove them. Bare uuids, which is the shape get_board returns…
- `position` (number): Sort key within the column, not an index — smaller sorts higher. Read the neighbours' `position` off get_board and send a number between them; omit it to append to the bottom. Positions come back as…
- `priority` (string): One of low, medium, high or urgent. Omit to leave it alone; there is no value here that clears a priority already set.
- `status` (string): Move it to this column, by name as shown on the board. Send this or `status_uuid`, not both. move_ticket is the tool for a move alone.
- `status_uuid` (string): Move it to this column, by uuid, from get_board. The exact form of `status`, unaffected by a column being renamed.
- `task_uuid` (string, required): The ticket's uuid, from list_tickets, get_board or search
- `title` (string): Replaces the title. Omit to leave it as it is.
- `version` (integer, required): The `version` from the ticket as you last read it. Not a number you choose or increment: send back exactly what get_ticket gave you. If somebody else has written since, the call answers 409 rather th…

### `move_ticket` (~429 tokens)

Move a ticket to another column. Give either `status` (the column name) or `status_uuid` — with NEITHER this is a 400 ("body must NOT have fewer than 2 properties") and not a no-op, and with both it is a 400 saying to pick one. A column name is matched case-insensitively and trimmed, so "to do" finds "To Do", but a name no column has is a 400 naming it, and a `status_uuid` belonging to another board is refused too — get_board is where both come from. `version` must be the one get_ticket returned; a stale one is a 409 that carries the current version with it, so the retry does not need another read. The ticket is appended to the bottom of the column it arrives in unless you send a `position`; it does not keep the number it had in the column it left.

Input parameters:

- `position` (number): Sort key within the column, not an index — smaller sorts higher. Read the neighbours' `position` off get_board and send a number between them; omit it to append to the bottom. Positions come back as…
- `status` (string): The column to move it to, by name as shown on the board. Send this or `status_uuid`, not both, and one of the two is required — there is no default target.
- `status_uuid` (string): The column to move it to, by uuid, from get_board. The exact form of `status`, and unaffected by a column being renamed.
- `task_uuid` (string, required): The ticket's uuid, from list_tickets, get_board or search
- `version` (integer, required): The `version` from the ticket as you last read it. Not a number you choose or increment: send back exactly what get_ticket gave you. If somebody else has written since, the call answers 409 rather th…

### `comment_on_ticket` (~218 tokens)

Add a comment to a ticket. Markdown in `body` is parsed — headings, lists and code fences all render. Comments are not versioned: this takes no `version` and cannot 409, so it is the one write that is always safe to make against a ticket somebody else is editing, and it is the right way to report something rather than editing the description out from under them. It is also the write a read-only key can still make — a `commenter` role is refused update_ticket and move_ticket with a 403 but allowed this. A ticket you cannot open is a 404. Posting does not mark the thread read on your behalf.

Input parameters:

- `body` (string, required): The comment, Markdown, 1 to 20000 characters. Posting is not idempotent here unless you send an Idempotency-Key, so on an unclear failure read the thread back with get_ticket_comments before trying a…
- `task_uuid` (string, required): The ticket's uuid, from list_tickets, get_board or search

### `update_comment` (~259 tokens)

Rewrite one of YOUR OWN comments. Only the author may edit a comment: somebody else's is a 404, exactly the same 404 as a comment that does not exist, so this never tells you who wrote what. It REPLACES the whole body rather than appending — read the comment back with get_ticket_comments and send it plus your addition if you meant to add a line. The edit is visible: the comment is marked as edited with the time, so this is not a way to make a change quietly. Comments are not versioned, so there is no `version` here and no 409 — the last edit wins, and two edits racing is simply the later one. An empty body is a 400 rather than a deletion; delete_comment is how you retract one.

Input parameters:

- `body` (string, required): The comment's new text, Markdown, 1 to 10000 characters. It replaces everything the comment said.
- `comment_uuid` (string, required): The comment's uuid, from get_ticket_comments — or from the reply comment_on_ticket gave you, which is the cheap way to hold on to one you just wrote.
- `task_uuid` (string, required): The ticket's uuid, from list_tickets, get_board or search

### `delete_comment` (~208 tokens)

Retract one of YOUR OWN comments. As with editing, only the author may do it and anybody else's comment is an indistinguishable 404. The comment leaves the thread immediately and goes to the workspace trash, where a person can restore it for 30 days — there is no tool here that restores one, so from an agent's side treat it as final. Deleting a comment nobody has replied to is unremarkable; deleting one that a colleague has already answered leaves their reply talking to nothing, so prefer update_comment with a correction when the thread has moved on. A comment already deleted is the same 404 again, which makes a retry safe and tells you nothing.

Input parameters:

- `comment_uuid` (string, required): The comment's uuid, from get_ticket_comments — or from the reply comment_on_ticket gave you, which is the cheap way to hold on to one you just wrote.
- `task_uuid` (string, required): The ticket's uuid, from list_tickets, get_board or search

### `mark_comments_read` (~186 tokens)

Clear this ticket's unread badge for the user this key acts as. Every other tool here leaves read state alone — get_ticket_comments deliberately does not mark anything read — so this is the one call that says "I have seen the thread", and the one that clears an unread count an agent's own comment created for the person whose key it is. It marks read up to the NEWEST comment that exists right now: a comment posted a second later is unread again, so this is a point in time rather than a standing subscription. Idempotent — calling it on a thread with nothing unread succeeds and changes nothing — and it reports `unread_comment_count: 0` with the moment it recorded. Reading is not affected: the thread is still fully readable afterwards.

Input parameters:

- `task_uuid` (string, required): The ticket's uuid, from list_tickets, get_board or search

### `add_subtask` (~194 tokens)

Add one item to a ticket's checklist. This is where acceptance criteria belong when they are meant to be ticked off one at a time rather than read as prose: they show as a progress count on the card, and update_subtask is how each one gets marked done. It appends to the bottom of the list. Plain text, not Markdown — an item is a line on a checklist rather than a document, so write the criterion as a sentence and put the detail in the ticket's description. One item per call; a checklist of seven is seven calls. A ticket you cannot open is a 404.

Input parameters:

- `task_uuid` (string, required): The ticket's uuid, from list_tickets, get_board or search
- `title` (string, required): The item's text, 1 to 500 characters. A name only — there is no description, no assignee and no due date on a checklist item.

### `update_subtask` (~278 tokens)

Tick a checklist item off, rename it, or move it up the list. `is_done: true` is the common one and is what makes the ticket's progress count move; `is_done: false` un-ticks an item that was ticked too early. Send at least one of the three — all three omitted is a 400. Renaming REPLACES the whole title rather than appending to it. An item on another ticket, or on a ticket you cannot open, is a 404 naming neither.

Input parameters:

- `is_done` (boolean): true ticks it off, false un-ticks it. Omit to leave the tick alone rather than sending false, which is a change.
- `position` (number): Sort key within the checklist, not an index — smaller sorts higher. Read the neighbours' `position` off the ticket's `subtasks` and send a number between them. Omit to leave the order alone.
- `subtask_uuid` (string, required): The item's uuid, from the ticket's `subtasks` in get_ticket — or from the reply add_subtask gave you.
- `task_uuid` (string, required): The ticket's uuid, from list_tickets, get_board or search
- `title` (string): Replaces the item's text. Omit to leave it as it is.

### `delete_subtask` (~170 tokens)

Remove an item from a ticket's checklist. This one is not recoverable — a checklist item does not go to the trash the way a ticket or a comment does, so a criterion deleted here is gone and has to be typed again. Tick it off with update_subtask when it is done; delete it only when it should never have been on the list. An item already deleted, one on another ticket, and a ticket you cannot open are all the same 404, so a retry is safe and confirms nothing.

Input parameters:

- `subtask_uuid` (string, required): The item's uuid, from the ticket's `subtasks` in get_ticket — or from the reply add_subtask gave you.
- `task_uuid` (string, required): The ticket's uuid, from list_tickets, get_board or search

### `upload_ticket_attachment` (~393 tokens)

Attach a file to a ticket — the way an agent puts evidence on a ticket rather than describing it. Two ways to give it the bytes, and exactly one must be used. `file_path` reads a file from the machine THIS SERVER runs on (normally the agent's own machine, since the client starts this as a subprocess) and is the right one for anything that already exists on disk; it costs no context, so prefer it. `text` is for content the agent has just written — a log, a CSV, a diff — and needs `filename` alongside it. Laver refuses anything that is not one of application/pdf, application/vnd.openxmlformats-officedocument.presentationml.presentation, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.openxmlformats-officedocument.wordprocessingml.document, image/gif, image/jpeg, image/png, image/webp, text/csv, text/plain, and refuses a file whose BYTES do not match the type its name claims, so renaming a zip to .png fails at the server rather than here. 25 MB is the ceiling. A workspace over its storage quota is a 402, which no retry fixes. Uploading is a write: a read-only role is a 403.

Input parameters:

- `content_type` (string): Overrides the type guessed from the filename's extension
- `file_path` (string): Path to an existing file on the machine running this server. Mutually exclusive with `text`
- `filename` (string): The name to store it under. Required with `text`; defaults to the basename of `file_path`. Its extension decides the content type
- `task_uuid` (string, required): The ticket's uuid, from list_tickets, get_board or search
- `text` (string): Literal file content, for something the agent wrote rather than something on disk. Requires `filename`

### `delete_ticket_attachment` (~169 tokens)

Remove a file from a ticket. This is the same one-way-but-recoverable move archive_ticket makes: the attachment leaves the ticket immediately and its bytes sit in the workspace trash for 30 days, after which the sweep destroys them. There is no tool here to restore one — that is the web app — so treat it as final in an agent's hands. The freed bytes stop counting against the workspace's storage quota straight away. An attachment already deleted, still uploading, or on a ticket this key cannot open are all the same 404, so this never confirms that a file existed.

Input parameters:

- `attachment_uuid` (string, required): The attachment's uuid, from list_ticket_attachments
- `task_uuid` (string, required): The ticket's uuid, from list_tickets, get_board or search

### `archive_ticket` (~219 tokens)

Move a ticket to the workspace trash — the recoverable half of deleting. It leaves the board, stops appearing in list_tickets and search, and get_ticket answers 404 for it, but it can be restored for 30 days from the workspace's trash in the web app, and is destroyed automatically once that window passes. This server has no restore tool, so from here archiving is a door only a person can reopen. It takes no `version` and cannot 409 — archiving is not a field edit, so there is nothing to conflict with. Already archived, never existed, and on a board you cannot open are all the same 404, so a retry is safe and a 404 tells you nothing about tickets you cannot see. If you intend to destroy the ticket permanently, call get_ticket FIRST and keep its `workspace_uuid`: delete_ticket needs one and an archived ticket can no longer be read to find it.

Input parameters:

- `task_uuid` (string, required): The ticket's uuid, from list_tickets, get_board or search

### `delete_ticket` (~303 tokens)

Permanently destroy a ticket that is ALREADY in the trash, along with its comments and attachments. Nothing undoes this — not the trash, not a restore, not support. It is the second step and never the first: archive_ticket, then this. A ticket still live on a board answers 404 rather than being destroyed, so this cannot bin something in one call. `workspace_uuid` must be the workspace the ticket belongs to — from get_ticket before it was archived, or get_board for the board it was on. Naming a workspace your key was not issued for is a 403 (`This API key is scoped to a different workspace.`) and never reaches the ticket; naming your own workspace for a ticket that does not live in it is a 404. Keep this for tickets that should never have existed: a duplicate, a probe, a test fixture you created. Anything a person might want back can simply be left archived, where it expires on its own. The 404 is otherwise deliberately undistinguished — not archived, already destroyed, and a board you cannot open all look identical.

Input parameters:

- `task_uuid` (string, required): The ticket's uuid, from list_tickets, get_board or search
- `workspace_uuid` (string, required): The workspace's uuid, from list_workspaces. A key only ever reaches the one workspace it was issued for, so this is that one; another workspace's uuid is a 404 rather than a permission error.

### `link_tickets` (~196 tokens)

Record that one ticket must be finished before another can start. Direction is from the point of view of task_uuid: "blocks" means task_uuid has to be done first. Every ticket read afterwards carries `blocked_by`, `blocks` and `is_blocked`, so this is how you work out what order to do things in.

Input parameters:

- `direction` (string, required): Read from task_uuid's side: 'blocks' means task_uuid must be finished before other_task_uuid can start; 'blocked_by' means the reverse. Unlink with unlink_tickets.
- `other_task_uuid` (string, required): The ticket at the far end of the link. Must be a different ticket from task_uuid.
- `task_uuid` (string, required): The ticket the link is stated from, and the one `direction` is read relative to. Swapping this with other_task_uuid reverses the meaning, so decide which end you are describing before you call.

### `unlink_tickets` (~222 tokens)

Remove the link between two tickets. Order does not matter — it finds the pair from either end, so you do not have to know which one is recorded as the blocker. Note what it removes: Laver can hold more than one kind of link between the same pair (a dependency, a related-to, a duplicate-of), a person can add the other kinds in the web app, and this removes ALL of them rather than only the dependency link_tickets creates. It is not an error to unlink a pair that was never linked — nothing matches, nothing is removed, and the call still succeeds — so this cannot be used to test whether a link exists; read `blocked_by` and `blocks` on the ticket for that. Either uuid being unreadable, on a board you cannot open, or nonexistent is a 404.

Input parameters:

- `other_task_uuid` (string, required): The ticket's uuid, from list_tickets, get_board or search
- `task_uuid` (string, required): The ticket's uuid, from list_tickets, get_board or search

### `create_board` (~254 tokens)

Create a board in a workspace. Without `template` the board arrives with Laver's default columns; with one it is seeded with that template's columns, labels and a few example tickets. `template` accepts exactly "crm" or "sales-leads" — anything else is a 400 from the schema, so do not guess an id. A template applies once, at creation: there is no way to apply one to an existing board and no link back afterwards, so a board created without one has to be arranged by hand. A workspace you cannot open is a 404, and a 402 means the plan's board limit is already reached and the board was not created — neither is worth retrying.

Input parameters:

- `name` (string, required): The board's name, 1 to 200 characters. Names are not unique, so creating the same one twice gives you two boards rather than an error.
- `template` (string): Omit for the default columns
- `workspace_uuid` (string, required): The workspace's uuid, from list_workspaces. A key only ever reaches the one workspace it was issued for, so this is that one; another workspace's uuid is a 404 rather than a permission error.

### `create_status` (~178 tokens)

Add a status column to a board. It is appended to the right-hand end; reorder_statuses is how it goes anywhere else. The reply carries the new column's uuid, which is what create_ticket and move_ticket take, so a column can be created and filled without re-reading the board. Names are not unique — creating "Review" twice gives you two columns called Review and no error — so read get_board first if you mean to check. A board you cannot open is a 404.

Input parameters:

- `board_uuid` (string, required): The board's uuid, from list_boards
- `name` (string, required): The column's name as it appears on the board, 1 to 100 characters. This is also what `status` accepts anywhere a column can be named instead of pointed at by uuid, so make it something worth typing.

### `update_status` (~301 tokens)

Rename a column, recolour it, or mark it as the one that means finished. `is_complete: true` is the interesting one: a board has at most ONE completed column, so setting it here silently clears the flag on whichever column held it, and the reply names what was unset. That flag is what `overdue` in list_workspace_tickets reads — a ticket sitting in a complete column is never overdue — so moving it changes what triage reports. Renaming is safe for tickets, which point at the uuid rather than the name, but it does break any saved text that named the old column. Send at least one of the three fields; all omitted is a 400 saying there is nothing to update.

Input parameters:

- `board_uuid` (string, required): The board's uuid, from list_boards
- `color`: The column's colour as a six-digit hex string, '#334455'. Explicit null clears it and the column falls back to the default for its position; omitting it leaves the colour alone. Those are different.
- `is_complete` (boolean): Whether tickets in this column count as finished. Only one column on a board may be the complete one, so true clears it elsewhere; false leaves the board with none.
- `name` (string): Replaces the column's name. Omit to leave it as it is.
- `status_uuid` (string, required): The column's uuid, from get_board's `statuses`

### `reorder_statuses` (~170 tokens)

Set the left-to-right order of a board's columns. Send the COMPLETE list of status uuids in the order you want — a partial list is refused rather than applied, which is what stops two people reordering at once from silently dropping a column. Read them off get_board immediately before calling, and if a column has been added or removed since, this answers 409 ("The column list changed. Refresh and try again."): re-read and send the new complete list. Nothing moves between columns; only the order changes.

Input parameters:

- `board_uuid` (string, required): The board's uuid, from list_boards
- `status_uuids` (array, required): Every column on the board, exactly once each, in the order you want them shown. Anything less than the whole set is a 409.

### `delete_status` (~171 tokens)

Remove a column from a board. It has to be EMPTY first: a column still holding tickets is a 409 saying how many, and the tickets are not moved or archived for you — move them with move_ticket and call this again. A board also has to keep one column, so deleting the last one is a 409 as well. Neither refusal is worth retrying unchanged. Unlike a ticket this does not go to the trash and cannot be restored, though the tickets that were in it are untouched because you moved them out first. A column that is not there, or a board you cannot open, is a 404.

Input parameters:

- `board_uuid` (string, required): The board's uuid, from list_boards
- `status_uuid` (string, required): The column's uuid, from get_board's `statuses`

### `create_group` (~136 tokens)

Add a group to a board — the horizontal swimlanes tickets are sorted into, as opposed to the status columns they move through. A ticket's group is set with update_ticket, not here. Appended to the bottom; reorder_groups moves it. There is deliberately no tool that renames a group: the API has no route for it, so a group named wrongly is deleted and made again. A board you cannot open is a 404.

Input parameters:

- `board_uuid` (string, required): The board's uuid, from list_boards
- `name` (string, required): The group's name, 1 to 100 characters. Not unique.

### `reorder_groups` (~128 tokens)

Set the top-to-bottom order of a board's groups. Same contract as reorder_statuses: send the COMPLETE list of group uuids from get_board in the order you want, and a list that no longer matches the board is a 409 ("The group list changed. Refresh and try again.") rather than a partial reorder. Tickets keep their groups; only the lanes move.

Input parameters:

- `board_uuid` (string, required): The board's uuid, from list_boards
- `group_uuids` (array, required): Every group on the board, exactly once each, in the order you want them shown.

### `delete_group` (~140 tokens)

Remove a group from a board. Unlike deleting a column this does NOT require the group to be empty: the tickets in it survive and are simply left ungrouped, which also bumps each of their versions — so anything holding a version for one of those tickets has just been made stale and must re-read before writing. The group itself does not go to the trash and cannot be restored. A group that is not there, or a board you cannot open, is a 404.

Input parameters:

- `board_uuid` (string, required): The board's uuid, from list_boards
- `group_uuid` (string, required): The group's uuid, from get_board's `groups`

### `list_custom_fields` (~169 tokens)

The custom fields defined on a board: each one's uuid, name, type and — for a select — its options. The uuid is the key `update_ticket`'s `custom_fields` is written against, and a value keyed by a field's NAME is dropped silently, so this is the read that makes writing one possible. get_board returns the same definitions alongside everything else; this is the cheap way to ask for them alone. Fields are board-scoped by design — 'Size' on one board is not 'Size' on another — so a uuid from one board written to a ticket on another is discarded without an error. Reading them needs only board access, where defining one needs a workspace admin.

Input parameters:

- `board_uuid` (string, required): The board's uuid, from list_boards

### `create_custom_field` (~381 tokens)

Define a custom field on a board — a column of structured data every ticket on that board can carry, in addition to its description. Types: text, number, date, checkbox, select, currency. A `select` REQUIRES `options` and is refused without at least one; a `currency` takes a three-letter ISO 4217 code in `currency` (checked against the real list, so 'ZZZ' is a 400). The type cannot be changed afterwards — there is no answer to what a text value should become as a number that is not a guess — so a field of the wrong type has to be deleted and made again, losing every value stored in it. Defining a field requires a workspace OWNER or ADMIN; an ordinary member who can edit tickets is a 403, even though that same member may fill the field in once it exists. The reply carries the new field's uuid, which is the key update_ticket writes values against.

Input parameters:

- `board_uuid` (string, required): The board's uuid, from list_boards
- `currency` (string): For `currency` only: an ISO 4217 code such as USD or GBP. Ignored by every other type.
- `name` (string, required): What the field is called, 1 to 60 characters. Unique per board — a name another field on this board already has is a 409 — and board-scoped, so the same name on another board is a different field.
- `options` (array): The choices, for `select` only and required by it — up to 40, each 1 to 60 characters. Ignored by every other type.
- `type` (string, required): What kind of value it holds. Fixed at creation and not editable afterwards, so choose it before you create the field rather than after.

### `update_custom_field` (~299 tokens)

Rename a custom field, change a select's options, change a currency field's code, or move it up the list. The `type` is deliberately not editable and is not a parameter here. `options` REPLACES the whole list rather than adding to it, and removing an option does not clear it from tickets that already hold that value — those keep a value the dropdown no longer offers, so read the tickets before you shorten a list. Send at least one field; all omitted is a 400. Requires a workspace OWNER or ADMIN, like defining one.

Input parameters:

- `board_uuid` (string, required): The board's uuid, from list_boards
- `currency` (string): For a `currency` field, its ISO 4217 code. A field of any other type is a 404 saying so.
- `field_uuid` (string, required): The field's uuid, from list_custom_fields or get_board
- `name` (string): Replaces the field's name. Omit to leave it as it is.
- `options` (array): For a `select`, the complete list of choices after the write — it replaces rather than adds. A field of another type takes it without complaining and then ignores it, so sending it there does nothing.
- `position` (number): Sort key among the board's fields — smaller sorts first. Read the neighbours' `position` off list_custom_fields and send a number between them.

### `delete_custom_field` (~161 tokens)

Remove a custom field definition from a board, and with it every value any ticket on that board held in it. This is the destructive one in this group: the values are not archived, do not go to the trash, and cannot be recovered by restoring anything — deleting a field to recreate it loses the data even if you spell the name identically. There is no confirmation and no dry run, so read list_custom_fields and decide first. Requires a workspace OWNER or ADMIN. A field that is not there, or a board you cannot open, is a 404.

Input parameters:

- `board_uuid` (string, required): The board's uuid, from list_boards
- `field_uuid` (string, required): The field's uuid, from list_custom_fields or get_board

### `list_labels` (~174 tokens)

Every label in the workspace, alphabetically, with its uuid and colour. This is the workspace-wide source of the uuids `update_ticket` takes as `label_uuids` — get_board only shows the ones already in use on that board, so a label another team applies elsewhere is invisible there and is here. Labels are shared across every board in the workspace: applying one to a ticket does not copy it anywhere, and renaming it changes it for everybody. An empty list means the workspace genuinely has none. A workspace this key cannot open is a 404.

Input parameters:

- `workspace_uuid` (string, required): The workspace's uuid, from list_workspaces. A key only ever reaches the one workspace it was issued for, so this is that one; another workspace's uuid is a 404 rather than a permission error.

### `create_label` (~249 tokens)

Create a workspace label. Names are unique ignoring case and surrounding space — 'Bug', 'bug' and ' bug ' are one label — so creating one that exists is a 409 rather than a second label. List_labels first and reuse the uuid you find rather than calling this to see whether it conflicts. A colour is REQUIRED — six-digit hex, '#d97757' — because a label with no colour is a label nobody can pick out on a board. The new label exists workspace-wide immediately and appears on every board, but is on no ticket until update_ticket puts it there. The reply carries its uuid, which is what `label_uuids` takes.

Input parameters:

- `color` (string, required): Six-digit hex with the leading hash, '#334455'. Required, and stored lower-case.
- `name` (string, required): The label's text, 1 to 50 characters. Unique across the workspace — a duplicate is a 409.
- `workspace_uuid` (string, required): The workspace's uuid, from list_workspaces. A key only ever reaches the one workspace it was issued for, so this is that one; another workspace's uuid is a 404 rather than a permission error.

### `update_label` (~237 tokens)

Rename or recolour a workspace label. It changes EVERYWHERE at once — every board, every ticket that carries it, for everybody in the workspace — because there is one label rather than a copy per board. That is the thing to be sure of before calling: a rename to something more specific may be wrong for whoever else is using it. The uuid does not change, so nothing holding one breaks. Renaming onto a name another label already has — ignoring case and surrounding space — is a 409. Send at least one of the two fields.

Input parameters:

- `color` (string): Replaces the colour. Six-digit hex, '#334455'.
- `label_uuid` (string, required): The label's uuid, from list_labels or from a board's `labels` in get_board
- `name` (string): Replaces the label's text. Omit to leave it as it is.
- `workspace_uuid` (string, required): The workspace's uuid, from list_workspaces. A key only ever reaches the one workspace it was issued for, so this is that one; another workspace's uuid is a 404 rather than a permission error.

### `delete_label` (~207 tokens)

Delete a workspace label and strip it from every ticket that carries it, across every board. There is no trash for a label and nothing here restores one: recreating it by name gives a NEW uuid on no tickets, so the association is gone even if the word comes back. The reply's `label.affected_task_uuids` names every ticket it was stripped from, which is the only record of what it was on — keep it if there is any chance of wanting to put the label back. A label that is not there, or a workspace this key cannot open, is a 404.

Input parameters:

- `label_uuid` (string, required): The label's uuid, from list_labels or from a board's `labels` in get_board
- `workspace_uuid` (string, required): The workspace's uuid, from list_workspaces. A key only ever reaches the one workspace it was issued for, so this is that one; another workspace's uuid is a 404 rather than a permission error.

### `list_automations` (~223 tokens)

The automation rules on a board — each with its trigger, conditions, actions, whether it is `enabled`, the user it runs as, and the `version` any edit would need. These are live: a rule with `enabled: true` fires on its trigger within a couple of seconds and applies its actions as the person named in `run_as_user_uuid`. Read this before creating a rule on a board you did not set up, both because the per-board limit counts what is already here (two on the free plan) and because an existing rule may already do what you were about to add. What a rule has actually done is list_automation_runs rather than this. A rule that switched itself off after looping shows up here as `enabled: false` with a `disabled_reason`. Board access is enough to read them, which is wider than creating one. A board you cannot open is a 404, the same 404 a board that never existed gives.

Input parameters:

- `board_uuid` (string, required): The board's uuid, from list_boards

### `create_automation` (~1230 tokens)

Create an automation rule on a board from a trigger, one to twenty actions, and optional conditions. READ THIS BEFORE CALLING IT: the rule runs as the user this API key acts as, every time it is triggered, for as long as it exists — a standing grant of that person's permissions to anybody who can cause the trigger, not a one-off write like every other tool here, and the resulting history is attributed to them. This one takes effect immediately: the rule is live as soon as it is created and fires on its trigger within a couple of seconds, so do not create one speculatively to see what it would do. Creating one requires a workspace OWNER or ADMIN — a member who can otherwise write on the board is a 403 and retrying cannot fix it. A 402 means the plan's per-board rule limit is already reached (two on free) and nothing was created. Conditions are a flat AND of field/operator/value tests and every one must hold; the operators a field accepts differ, so `title contains x` is valid where `title is x` is a 400 listing the operators title takes. `run_as_user_uuid` defaults to the key's own user, and naming somebody else is refused unless they are an active member who can write and can see the board. Rules arrive switched on unless you pass `enabled: false`. The `schedule` and `ticket.due` triggers are not events and REQUIRE `trigger_config`: a schedule fans out over every ticket the conditions select at its appointed time, in UTC, so give it conditions unless you really mean the whole board; a due-date rule fires once per ticket per threshold and never retroactively, so creating one does nothing to work that is already overdue. `column.empty` is the odd one: it is about ONE ticket rather than the board, so it requires a `task_uuid` alongside `trigger_config: {status_uuid}`, and it fires the moment that column holds nothing — which may be the moment you create it, since a column that is already empty is already empty. It also keeps doing it every time that column empties aga…

Input parameters:

- `actions` (array, required): Each action is an object with a `type` from the list and whatever that action needs — a status, a user, a label, a comment body
- `board_uuid` (string, required): The board's uuid, from list_boards
- `conditions` (array): A flat AND — every condition must hold. No OR and no nesting. Omit for every occurrence of the trigger
- `enabled` (boolean): Whether the rule runs. Defaults to true, so a rule you are still assembling is live the moment it is created — pass false and enable it once the actions are right.
- `name` (string, required): What the rule is called, 1 to 200 characters. It appears in the board's automation list and on every run record, so name it after what it does rather than after the trigger.
- `run_as_user_uuid` (string): Defaults to the user this key acts as. Read the warning above
- `run_limit` (integer): column.empty only, and every other trigger refuses it. How many times the rule may move the ticket before it retires itself: a ticket can leave that column and the column can empty again, and without…
- `task_uuid` (string): column.empty only, where it is required: the ticket the rule moves. Every other trigger refuses it, since those act on the ticket that set them off. Cannot be changed afterwards — a rule about the wr…
- `trigger_config` (object): Required for the schedule, ticket.due, ticket.in_column and column.empty triggers and ignored by every other one. A schedule needs {interval, at}; a due-date rule needs {when} plus {days} unless when…
- `trigger_type` (string, required): What starts the rule. Most are occurrences and need no trigger_config. Four are not: `schedule` and `ticket.due` require one, `ticket.in_column` needs {status_uuid, days} because sitting in a column…

### `get_automation` (~203 tokens)

One automation rule in full — its trigger and trigger_config, its conditions, its actions, whether it is `enabled`, the user it runs as, and the `version` that update_automation and delete_automation require. Read it immediately before either write: the version is the whole point of this call, and a version read minutes ago may already be stale. Board access is enough, but a non-admin sees a `call_webhook` action's url and body as "[hidden]" — the rule still reads correctly, and sending those redacted values back through update_automation would overwrite the real url with the word, so never round-trip an action list you did not read as an admin. A rule on another board, or a board you cannot open, is the same 404.

Input parameters:

- `board_uuid` (string, required): The board's uuid, from list_boards
- `rule_uuid` (string, required): The rule's uuid, from list_automations

### `list_automation_runs` (~420 tokens)

What a rule has actually done — newest first. This is the tool for "why did this ticket move?" and "why is my rule not firing?", and the answers come from different halves of it. Each run carries its `status`, the ticket it acted on with that ticket's title, `actions_done` out of `actions_total`, an `error` when one failed, and the times it was enqueued, started and finished. The runs that did nothing because the conditions did not hold are `no_match`, and they are EXCLUDED by default — a rule with conditions produces far more of them than real runs, so they would bury the interesting rows; pass `include_no_match: true` when the question is why nothing happened, because then they are the entire answer. `depth` above zero means the run was set off by another rule's write rather than by a person, and a run stopped at the depth cap is how a rule that feeds itself shows up. A ticket that has since been deleted or moved to another board keeps its run with a null title rather than vanishing. Deleting a rule destroys its history with it. Board access is enough to read this — deliberately, since being told why the board did something to your ticket is not the same power as writing rules.

Input parameters:

- `board_uuid` (string, required): The board's uuid, from list_boards
- `include_no_match` (boolean): Include the runs where the trigger fired and the conditions did not hold. Off by default. Turn it on when the complaint is that a rule never runs — a `no_match` row proves it is being triggered and f…
- `limit` (integer): How many runs to return, 1 to 200, newest first. Defaults to 50. There is no paging beyond this, so an older run than the limit reaches is not retrievable here.
- `rule_uuid` (string, required): The rule's uuid, from list_automations

### `update_automation` (~655 tokens)

Edit a live automation rule: rename it, switch it off, change what triggers it, or replace its conditions or actions. `enabled: false` is the one to reach for first when a rule is misbehaving — it stops the rule immediately and reversibly, where deleting it also destroys its run history, so pause before you delete and read list_automation_runs while you still can. `conditions` and `actions` REPLACE the stored lists rather than merging into them, so send the complete array you want, built from what get_automation returned and not from memory. Requires a workspace OWNER or ADMIN and the current `version`; a stale version is a 409 carrying the current one, so re-read with get_automation and reapply rather than retrying blind. Send `version` plus at least one real change — a version on its own is a 400. `trigger_type` and `trigger_config` are two halves of one meaning: changing either alone is checked against the stored other half and refused if the pair would not make sense. Two things cannot be changed at all and are not parameters here: a rule's `task_uuid` and its `run_limit`, so a column.empty rule pointed at the wrong ticket has to be deleted and made again. Switching a rule back on clears the `disabled_reason` that a circuit breaker left, but does not give a retired run_limit rule any more runs.

Input parameters:

- `actions` (array): The complete action list after the write, replacing the stored one. Never build it from a rule you read as a non-admin: a `call_webhook` url comes back as "[hidden]" there and would be written back l…
- `board_uuid` (string, required): The board's uuid, from list_boards
- `conditions` (array): The complete condition list after the write — an empty array removes every condition and makes the rule fire on every occurrence of its trigger, which is usually not what someone narrowing a rule mea…
- `enabled` (boolean): Whether the rule runs. False is the reversible way to stop a rule; it keeps the definition and the run history.
- `name` (string): Renames the rule.
- `rule_uuid` (string, required): The rule's uuid, from list_automations
- `run_as_user_uuid` (string): Who the rule acts as from now on. It is a standing grant of that person's permissions to anybody who can cause the trigger, and every future run is attributed to them, so repointing a rule is a bigge…
- `trigger_config` (object): Replaces the trigger's configuration, in the same shape create_automation documents. Changing a schedule's interval or time recomputes when it next fires; enabling or disabling a rule deliberately do…
- `trigger_type` (string): Changes what starts the rule. If the new trigger needs a trigger_config, send that too — the pair is validated together against whatever is stored.
- `version` (integer, required): The rule's version as get_automation last returned it. Required. A mismatch is a 409 whose body carries the current version.

### `delete_automation` (~251 tokens)

Delete an automation rule. It stops firing at once and the rule is gone — there is no trash for one and nothing here restores it, so a rule deleted by mistake has to be rebuilt from what you read before deleting. It also takes the rule's ENTIRE RUN HISTORY with it: after this, list_automation_runs has nothing, and the record of what the rule did to which tickets survives only in the workspace audit log. If the goal is to stop a rule rather than to be rid of it, update_automation with `enabled: false` does that reversibly and keeps the history. Requires a workspace OWNER or ADMIN and the current `version`; a stale version is a 409 carrying the current one, which is the guard against deleting a rule somebody else has just changed under you. The reply is empty on success.

Input parameters:

- `board_uuid` (string, required): The board's uuid, from list_boards
- `rule_uuid` (string, required): The rule's uuid, from list_automations
- `version` (integer, required): The rule's version as get_automation last returned it. Required, for the same reason editing needs one: what you discard should be what you last looked at.

### `list_published_links` (~377 tokens)

What of this workspace's is on the public internet right now: `wiki_pages` and `boards`, each with its `public_token` — the share link's secret — plus who published it, when, and how many times a stranger has viewed it. This is the inventory read, and no other tool answers it, since a page or board reads exactly the same here whether or not the world can see it. Two fields carry the meaning. `live` is what a stranger actually gets, so a row is not proof of reachability; when it is false, `dark_reason` says why — `page_deleted`, `wiki_deleted`, `board_deleted`, `page_private`, `workspace_switch_off`, or `unavailable` for a cause an admin cannot act on, such as a plan that no longer includes publishing. An archived page still holding a token is listed on purpose: it goes straight back onto the internet the moment somebody restores it, and that state has no other way of being found. `public_wiki_pages_enabled` and `public_boards_enabled` are the workspace-wide switches, so every row can be dark for one reason. At most 500 rows of each; `truncated` says if that happened. Requires a workspace OWNER or ADMIN — an ordinary member is a 403, a non-member a 404 — and it grants no access a reader did not already have, since every link here is public by definition. Taking one back down is deliberately not a tool: report what is published and let a person decide what to unpublish.

Input parameters:

- `workspace_uuid` (string, required): The workspace's uuid, from list_workspaces. A key only ever reaches the one workspace it was issued for, so this is that one; another workspace's uuid is a 404 rather than a permission error.

### `list_wikis` (~250 tokens)

The wikis in a workspace, with their uuids — the only place a `wiki_uuid` comes from, so every other wiki tool starts here. `workspace_uuid` is required; start from list_workspaces if you do not have one. An empty list is not proof the workspace has no wiki: a key acting as a guest is excluded from workspace-wide reads and sees nothing here. Archiving a wiki (done in the browser; there is no tool here for it) takes it out of this list entirely, along with every page under it — pass `archived: true` to see those instead, which is also the only way to find a `wiki_uuid` for restore_wiki.

Input parameters:

- `archived` (boolean): Omit or false for the normal list. true swaps it for archived wikis instead — the two never mix in one reply, the same way an archived wiki never appears beside a live one in the workspace sidebar.
- `workspace_uuid` (string, required): The workspace's uuid, from list_workspaces. A key only ever reaches the one workspace it was issued for, so this is that one; another workspace's uuid is a 404 rather than a permission error.

### `restore_wiki` (~158 tokens)

Undo an archive: brings a wiki, and every page under it, back into list_wikis and back onto the internet if any of its pages were published. `wiki_uuid` comes from list_wikis with `archived: true` — that is the only place one is visible at all, since an archived wiki is invisible everywhere else this server reads. A wiki that is not archived, one in a workspace this key cannot open, and a uuid that does not exist are all the same 404, so this never confirms that a wiki exists. There is deliberately no tool here that archives one in the first place: that half stays a browser action.

Input parameters:

- `wiki_uuid` (string, required): The wiki's uuid, from list_wikis

### `search_wiki` (~221 tokens)

Full-text search inside ONE wiki, returning matching pages with a highlighted excerpt. A title match outranks a body match, so the page actually about the term comes first. A single word also matches as a prefix — 'custom' finds 'Customark' — while a multi-word query keeps the usual search semantics: "quoted phrase", -excluded, and OR. Matches are marked in the excerpt with the control characters \x01 and \x02 around each hit, not with markup; strip them before showing the text to anyone. `wiki_uuid` comes from list_wikis, and a wiki that does not exist or that you cannot open is the same 404. To search tickets and wikis together across a whole workspace, use `search` instead.

Input parameters:

- `q` (string, required): What to look for across this wiki's page titles and bodies, 1 to 500 characters. Scoped to the one wiki — use search to cover boards and tickets as well.
- `wiki_uuid` (string, required): The wiki's uuid, from list_wikis

### `get_wiki_tree` (~253 tokens)

Every page in a wiki as a tree — titles, uuids and nesting — without any page content. This is the cheap way to find a page uuid when you know roughly where it sits; search_wiki is the way when you know roughly what it says. Follow up with get_wiki_page for the content of one. A wiki you cannot open is a 404, the same one a wiki that does not exist gives. A big wiki is tens of thousands of tokens of metadata, so narrow it with `parent_page_uuid` and `depth` rather than reading all of it to find one uuid.

Input parameters:

- `depth` (integer): How many levels of nesting to return. 1 is the top level alone — the whole wiki's top-level pages, or just `parent_page_uuid` itself when one is given; 2 adds their children. Omit for every level.
- `parent_page_uuid` (string): Return only this page and everything nested under it. The uuid must be a page in THIS wiki — anything else is an error naming it, not an empty tree. Omit for the whole wiki.
- `wiki_uuid` (string, required): The wiki's uuid, from list_wikis

### `get_wiki_page` (~176 tokens)

One wiki page in full, with its content and any attachments it references. Get `page_uuid` from get_wiki_tree or from a search_wiki hit — a page uuid is not a wiki uuid, and passing one for the other is a 404. A page you cannot open is that same 404, so it never confirms that a page exists. This server can create a page (create_wiki_page) and add to the end of one (append_wiki_page), but cannot change or delete what is already written: to correct something, append the correction rather than planning to edit the page.

Input parameters:

- `page_uuid` (string, required): The wiki page's uuid, from get_wiki_tree or search_wiki. A page you cannot open is a 404, the same 404 as one that never existed.

### `get_wiki_page_version` (~237 tokens)

What a wiki page said at an earlier version — its title, content and who saved it. This is how you recover something that was overwritten, and it is a READ: the page is not changed and its version does not move. Reach for it the moment you find that a page no longer says what you put there. `version` counts from 1 and goes up by one on every save; the current version is on the page from get_wiki_page. A version that was never saved, and a page you cannot open, are both the same 404. There is deliberately no tool here that puts an old version back — read it and append the wording you want, because a restore overwrites whatever a colleague has open in the live editor right now.

Input parameters:

- `page_uuid` (string, required): The wiki page's uuid, from get_wiki_tree or search_wiki. A page you cannot open is a 404, the same 404 as one that never existed.
- `version` (integer, required): Which save to read. 1 is the page as first created; get_wiki_page reports the current number

### `create_wiki_page` (~470 tokens)

Write a NEW page into a wiki, with its body as markdown. This is how an agent puts findings somewhere durable instead of handing them back as chat text. `wiki_uuid` comes from list_wikis; `parent_page_uuid` (from get_wiki_tree) nests the new page under an existing one and is where a page belongs unless it is genuinely top-level. The markdown is converted server-side by the same parser ticket descriptions and comments go through — headings, lists, tables, code blocks, blockquotes, horizontal rules and links all survive. An `![alt](https://…)` image survives too, as a reference to that URL — but only for a URL already hosted somewhere public, because there is no tool here to upload an attachment, and an image the reader cannot fetch renders as a broken one. Raw HTML is kept as literal text rather than interpreted, so do not reach for it to get something markdown lacks. ADD ONLY: there is deliberately no tool to change or delete what is already on a page. Wiki pages have a live collaborative editor behind them, so a whole-document overwrite from here would silently destroy whatever a person had open at the time. Adding cannot damage anything, so it is offered and overwriting is not. Do not call this twice to 'update' a page; you will get two pages — to add to a page that already exists, use append_wiki_page. Creating a page needs write access to the workspace: a read-only role or a guest key is a 403 and retrying cannot fix it. A title is required and a body is not, so a page can be created empty and filled in by a person later.

Input parameters:

- `content_markdown` (string): The page body as markdown. Omit for an empty page. Longer than 100k characters is refused rather than truncated
- `parent_page_uuid` (string): Nest under this page. Must be in the same wiki — a page from another wiki is a 400, not a silent move
- `title` (string, required): The page's title, 1 to 500 characters. This is what the tree and search results show, so make it findable rather than clever.
- `wiki_uuid` (string, required): The wiki's uuid, from list_wikis

### `append_wiki_page` (~353 tokens)

Add markdown to the END of a page that already exists. This is the tool for writing what you learned into your own section of a shared page — the thing create_wiki_page cannot do without leaving you a second page of the same name. It ADDS ONLY: it cannot change or remove a word that is already on the page, which is exactly why it is safe to offer where a general edit is not. There is still no tool to edit or delete existing content; if you need to correct something you appended, append the correction. Takes NO version and never conflicts — two agents appending to the same page at the same moment both get their text, in whichever order the server serialises them, and neither is asked to retry. Markdown is converted server-side by the same parser create_wiki_page uses, so headings, lists, tables, code blocks and links all survive; lead with a heading if you want your section to be findable. The reply is the page's identity and its new version, deliberately NOT the page body — appending does not need you to have read the page, and getting the whole document back is the cost this tool exists to avoid. Markdown that is only whitespace is a 400 rather than a version bump for no change. Needs write access to the workspace: a read-only role or a guest key is a 403 and retrying cannot fix it.

Input parameters:

- `content_markdown` (string, required): Appended after everything already on the page. Longer than 100k characters is refused rather than truncated
- `page_uuid` (string, required): From get_wiki_tree or a search_wiki hit. A wiki uuid passed here is a 404, not a page

## Diagnostics

Captured diagnostic sections: Provenance, Dependencies. The full working is on the page: https://verifymcp.io/servers/developyn-laver-mcp/laver-mcp#diagnostics

## Score history

- 2026-08-18: 68
- 2026-08-17: 68
- 2026-08-16: 67
- 2026-08-15: 65
- 2026-08-14: 65

## Links

- npm package: https://www.npmjs.com/package/@laver/mcp
- Socket report: https://socket.dev/npm/package/@laver/mcp
- Repository: https://github.com/Developyn/laver-mcp
- Website: https://laver.app/
- Changelog RSS feed: https://verifymcp.io/servers/developyn-laver-mcp/laver-mcp.xml
- Changelog JSON feed: https://verifymcp.io/servers/developyn-laver-mcp/laver-mcp.json
- HTML version of this page: https://verifymcp.io/servers/developyn-laver-mcp/laver-mcp
