Sign In

@laver/mcp

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@laver/mcp

MCP server for Laver — drive kanban boards, tickets and the wiki from an agent.

Source
npmnpm
Version
0.1.0
Version published
Weekly downloads
250
-40.48%
Maintainers
1
Weekly downloads
 
Created
Source

@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.

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 install and nothing to export.

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

ToolWhat it gives you
list_workspacesWhere to start when you have no uuids
list_boardsThe boards in a workspace
get_boardA board with its status columns, labels, members and tickets
list_ticketsTickets on a board, filterable, paged — updated_since is how you follow a board
get_ticketOne ticket in full, including its version
get_ticket_commentsComments and activity history
searchTickets, wiki pages and comments across a whole workspace at once

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.

Writing

ToolNotes
create_ticketstatus takes the column name, or pass status_uuid
update_ticketNeeds version
move_ticketNeeds version, and a column — neither column is a 400
comment_on_ticketMarkdown in body is parsed; no version, so it cannot 409
archive_ticketTo the trash — recoverable for 30 days
delete_ticketDestroys one already in the trash — permanent
create_boardOptionally from a template — crm or sales-leads
link_tickets"this before that" — direction is blocks or blocked_by
unlink_ticketsFrom either end, and removes every kind of link on the pair

Wiki

list_wikis, search_wiki, get_wiki_tree, get_wiki_page.

Removing a ticket

Two steps, deliberately, so that nothing is destroyed by a single call:

archive_ticket  task_uuid                    → the workspace trash, recoverable for 30 days
delete_ticket   workspace_uuid + task_uuid   → gone, and nothing brings it back

delete_ticket refuses anything that is not already archived, so the order is enforced by the server rather than by convention. There is no restore tool here — a ticket in the trash is put back from the web app — so treat archive_ticket as the furthest you can go on your own.

Read the ticket before you archive it if you intend to destroy it: delete_ticket needs the workspace_uuid, get_ticket is where you get one, and an archived ticket can no longer be read.

Working out what to do next

Every ticket read carries blocked_by, blocks and is_blocked. is_blocked is false once every blocker has reached a completion column, so the tickets a board is ready for are the ones where it is false. link_tickets records the dependency; a link that would make a loop is refused with a 409, because a loop makes the ordering unanswerable.

The one rule worth knowing

Tickets carry a version. Every write must send the version you read, and a write against a stale one is refused with 409 rather than silently overwriting whoever got there first. The server turns that into an instruction, and Laver's refusal carries the current version, so the instruction can include it rather than spending a second call on it:

Laver 409: Task was updated by another request.

Somebody wrote first, so the version you sent is stale. The current version is 12. If your change does not depend on what you read — moving a ticket to a named column, say — retry with that version. If it does, call get_ticket again and decide against the ticket as it now is, or you will quietly undo the other write.

Read, then write. Do not cache a version across a long turn.

When the key is refused

A 401 is the key: missing, mistyped, revoked, expired, or a placeholder that was never filled in. The underlying message is not always a fair description of what happened — a key the JWT layer cannot parse comes back as "Authorization token is invalid: The token is malformed", which sounds like a corrupted string when the usual cause is simply a key that was replaced. The server appends what to do about it, including the part that catches people out:

An MCP client reads that environment once, when it starts this server, so it must be restarted afterwards — editing the config in a running session changes nothing.

A 403 is different and is never worth retrying: the key was accepted, and then refused this particular action. It is scoped to another workspace, or the person it acts as has a read-only role, or is a guest without access to that board.

The key itself is read in exactly one place, sent as a bearer token, and never logged, echoed, or included in any error text.

Checking it

node check.js                  # schema, then every read-only tool actually called
node check.js --require-live   # …and a skipped sweep is a failure, for CI

# the same calls against the API the published package actually talks to
LAVER_API_KEY_FILE=../.env node check.js --live-api

Two halves. The first is static: every tool registered, classified read or write, described, and given a schema. The second boots the backend from ../backend on a spare port, creates a workspace of its own, mints a key against it, and calls every read-only tool — through the tool's own zod schema and then its handler — sending every parameter the tool declares.

That half exists because the first one passed while list_wikis sent ?workspace= at a route that requires workspace_uuid. It 400'd on every call it ever made, and since it is the only tool that yields a wiki_uuid, the whole wiki half of this server was unreachable from the day it shipped — with registration, descriptions and schema shape perfect throughout.

It needs the same things npm test in backend/ needs: that directory, its node_modules, its .env, and the Postgres they point at. No API key and no network beyond localhost — a real LAVER_API_KEY in the environment is ignored. Without a backend it prints a banner saying the tools were not called and runs the schema half alone; --require-live turns that into a failure.

It is only as local as backend/.env is, though. Running it writes to whatever database that file points at: it creates a workspace, a board, two tickets, a comment, a wiki, a page and an API key, and deletes them again at the end. It also loads the backend into its own process. It listens with app.server.listen rather than app.listen — the same idiom the collab and board-events integration tests use — so Fastify's onListen hooks do not fire and none of the seven schedulers start; without that, the billing sweep alone would run against every workspace in that database. Point backend/.env at staging and this is a check that writes to staging.

The sweep is read-only and stays that way: a check that creates tickets in somebody's workspace every time it runs is a check people stop running. The write tools are covered by the schema half only — see the note at the foot of check.js for the way to cover them without sending a write.

Ctrl-C is safe. The sweep stops after the call in flight and the fixture workspace is deleted before the process exits; a second Ctrl-C kills it outright if the call in flight is the thing that is stuck.

Against the deployed API

--live-api points the same calls at LAVER_API_URLhttps://api.laver.app unless you say otherwise — with a real key, taken from LAVER_API_KEY or from the file LAVER_API_KEY_FILE names, exactly as the server itself takes it.

It exists because a green local run and a working published package are two different claims. The local sweep proves the tools agree with the code in front of you; this package talks to the deployed API, so a route that ships a rename before the package does breaks every agent in the field while the local sweep stays green. That is a narrow window — the tools and the routes live in one repo and move together — but it is exactly the window publishing to npm opens.

Both modes run the same table, in cases.js, and every case carries an expectation for each: exact counts locally, where the fixture is known, and shapes and invariants live, where the workspace is somebody's real one and cannot be seeded or torn down. A case with only one of the two is a failure, so a new call cannot cover one transport and skip the other.

It creates nothing and deletes nothing, and that is enforced rather than promised: live mode replaces fetch with one that refuses any method but GET, so a write tool called by mistake cannot reach the network at all. frontend/tests/check-mcp-live-api-mode.mjs runs the whole mode against a stub API and asserts that every request that left the process was a GET.

Instead of a fixture it goes looking for something to point at, and wants a board with at least two tickets in at least two columns, in a workspace with a wiki that has a page. It refuses to run against anything thinner rather than passing quietly: a filter case against an empty board passes whether or not the filter was applied, which is the failure this whole file exists to prevent.

Opt-in, and never part of npm run check:all or CI — it needs a key and a network, and neither belongs in a check that runs on a box with no secrets.

Publishing

Not yet published. The package is ready to be; the publish itself is the owner's to run, because it is public, permanent enough to matter, and takes a name nobody else can then have.

The name. laver on npm is taken — v1.0.0, published in 2021 by an unrelated maintainer — so the bare name is not available and never will be. This package is therefore @laver/mcp: the brand name kept as the scope, with the generic part where it belongs. Checked against the registry on 6 Aug 2026 — @laver/mcp is free, and nothing has ever been published under the old unscoped laver-mcp, so the rename costs nothing.

The scope has to exist first, and it does not yet. https://registry.npmjs.org/-/org/laver is a 404. A @laver/* package can only be published by an account for which laver is either its own username or an organisation it belongs to, so before the first publish:

  • create the laver org at https://www.npmjs.com/org/create — free for public packages — or confirm laver is the publishing account's username;
  • then npm whoami and check the account is a member of it.

npm publish fails with 404 Not Found - PUT https://registry.npmjs.org/@laver%2fmcp if the scope does not exist, which reads like a network fault rather than a missing org. That error is this step, not a broken package.

The executable stays laver-mcp. The package is @laver/mcp, but bin is deliberately not renamed to mcp: a global install would put a command called mcp on the PATH, which is far too generic and collides with every other MCP server anyone installs. npx -y @laver/mcp works regardless — npx runs the package's only bin whatever it is called — so nothing in the config snippet above depends on the command's name.

repository and bugs are deliberately absent. github.com/Developyn/laver is private, and npm renders those fields as links on the package page — pointing the only two "where does this come from" links at a 404 is worse than having neither. homepage is https://laver.app, which is public and answers. If the mcp/ directory is ever mirrored to a public repo, add them back:

"repository": { "type": "git", "url": "git+https://github.com/<org>/<repo>.git", "directory": "mcp" },
"bugs": { "url": "https://github.com/<org>/<repo>/issues" }

Before the first one

  • The laver scope existing and the publishing account belonging to it — see "The scope has to exist first" above. This is the one that bites.
  • 2FA on that account, if it is set to require it for publishing (npm enforces this for some accounts and packages and prompts for others). Passing --otp saves a prompt from failing a non-interactive run; drop it if not enrolled.
  • npm whoami answering with that account — npm login if not.
  • For CI instead of a laptop: an automation token in NPM_TOKEN (granular, write-scoped to this package). Automation tokens bypass the 2FA prompt, which classic read-write tokens do not.
  • Decide the version. It currently says 0.1.0, which is honest for a first release and signals the tool surface may still move; npm version 1.0.0 first if it should not.

The publish

cd mcp
npm ci                    # the lockfile, not whatever resolves today
npm run check             # schema + every read-only tool actually called
npm pack --dry-run        # confirm the file list is LICENSE, README.md, package.json, server.js
npm publish --access public --otp=<code-from-your-authenticator>

--access public is required here: scoped packages default to restricted, and a restricted publish on a free account is refused outright. publishConfig in package.json already sets it, so the flag is belt and braces rather than the only thing standing between this and a private package.

Afterwards

npx -y @laver/mcp         # should start and wait on stdio, not exit
npm view @laver/mcp

A mistake is recoverable only briefly: npm unpublish @laver/mcp@<version> works within 72 hours, and the version number is burned afterwards regardless. The package name is not returned to the pool by unpublishing a version.

Licence

MIT — see LICENSE, which ships in the package.

Keywords

mcp

FAQs

Package last updated on 06 Aug 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts