
Research
/Security News
77 Firefox Extensions Linked to Crypto Wallet and Credential Theft
Socket uncovered 77 linked Firefox extensions, including 40 that steal wallet secrets or credentials and 37 deceptive sports-score shells.
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".
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.
.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.
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 |
get_ticket | One ticket in full, including its version |
get_ticket_comments | Comments and activity history |
search | Tickets, 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
| Tool | Notes |
|---|---|
create_ticket | status takes the column name, or pass status_uuid |
update_ticket | Needs version |
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 |
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 |
Wiki
list_wikis, search_wiki, get_wiki_tree, get_wiki_page.
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.
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.
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.
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.
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.
--live-api points the same calls at LAVER_API_URL — https://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.
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:
laver org at https://www.npmjs.com/org/create — free for
public packages — or confirm laver is the publishing account's username;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
laver scope existing and the publishing account belonging to it — see
"The scope has to exist first" above. This is the one that bites.--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.NPM_TOKEN (granular,
write-scoped to this package). Automation tokens bypass the 2FA prompt, which
classic read-write tokens do not.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.
MIT — see LICENSE, which ships in the package.
FAQs
MCP server for Laver — drive kanban boards, tickets and the wiki from an agent.
The npm package @laver/mcp receives a total of 247 weekly downloads. As such, @laver/mcp popularity was classified as not popular.
We found that @laver/mcp demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

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.

Research
/Security News
Socket uncovered 77 linked Firefox extensions, including 40 that steal wallet secrets or credentials and 37 deceptive sports-score shells.

Security News
NIST disclosed an unreleased AI tool called V-etalon and opened a broad inquiry into NVD modernization after years of automation plans produced no public enrichment system.

Security News
In his AI Council 2026 talk, Feross Aboukhadijeh covers recent package compromises, vulnerability discovery, and a more automated security model.