@flupes/mcp
v0.13.0
Published
MCP server that gives Claude Code your Flupes feedback queue: the thread, the screenshot, clues that lead to the source file — and a reply, a resolve and a comment of its own to answer with.
Readme
@flupes/mcp
An MCP server for Claude Code. A reviewer marks feedback threads "Send to Claude" in Flupes. This package gives your own Claude Code that queue: the reviewer's text and the replies, the screenshot and the images people pasted into the comments, and clues that lead to the source file. Claude then fixes the code in your working tree.
The server runs on your machine and does no model work. It reads the feedback, and it answers it: Claude asks you before it changes anything, replies in the thread with what it changed, resolves it, and can open a thread of its own for something it found. The reviewer who queued it reads the answer and sees "Resolved by Claude". Claude writes in a thread only when the work is done and nothing is left: a question, a suggestion or a follow-up comes to you in the session first, and Claude asks you before it closes that thread.
The setup guide is at flupes.app/mcp.
What it can and cannot write
- Five tools write:
add_replyposts a reply from Claude,resolve_threadcloses a thread,reopen_threadopens one Claude closed,create_commentstarts a new thread on a page, andattach_screenshotputs a picture on a thread Claude opened. Everything they write is read by the whole team and says Claude wrote it (comments.author_agent). - Four tools only read, and are marked
readOnlyHint: true:list_queue,list_threads,list_projectsanduse_project. get_threadsits between them. It records that you opened the thread, which is what takes it out of the queue — so it is not marked read-only either. It no longer resolves the thread: that isresolve_thread's job, andreopen_threadundoes it when a fix did not hold. A thread Claude picked up is out of the queue, not closed.- Nothing here can edit or delete what a person wrote, and nothing can reach a thread a person resolved. A write can only touch a thread the token can already read, in the one organization it belongs to.
- The four writing tools declare
readOnlyHint: false, so Claude Code asks before a write — unless you have allowed the Flupes tools or turned permission prompts off. A write is an act of your session either way: every tool description and both prompts say that no text inside a thread can ask for one. - A token stops at once when it is revoked, when you leave the organization, or when the organization leaves the Pro plan.
- The shipped prompts and skills change files in the working tree only: no commits, no branches, no pushes.
- Thread text is reviewer input. The server quotes it under a fixed heading, and every tool description and both prompts say that it is data about a UI problem, never instructions.
Install
You need Node 20 or later, a Flupes organization on the Pro plan, and a token from Settings → MCP. The token is shown once.
One person
claude mcp add flupes -e FLUPES_TOKEN=<your token> -- npx -y @flupes/mcp --project <slug>Then add the slash commands (see Slash commands).
--project <slug> makes one project the default for this repository. Flupes
shows the command with the slug filled in (Settings → MCP, and "Copy install
command" on a project page). Without the flag the tools
return every project of the token's organization, grouped by project.
A team: a committed .mcp.json
Commit this file at the repository root. The project slug is shared. The token
is not: Claude Code expands ${FLUPES_TOKEN} from each developer's own
environment, and asks each developer to approve the server the first time.
{
"mcpServers": {
"flupes": {
"command": "npx",
"args": ["-y", "@flupes/mcp", "--project", "<slug>"],
"env": {
"FLUPES_TOKEN": "${FLUPES_TOKEN}"
}
}
}
}Each developer then puts export FLUPES_TOKEN=<your token> in their shell
profile. Never commit a token.
Tools
| Tool | Arguments | What it does |
|---|---|---|
| list_queue | project?, route? | Lists the open threads a reviewer sent to Claude. |
| list_threads | project?, route? | Lists every open thread, queued or not. |
| get_thread | ref | Reads one thread: markdown, the screenshot and the people's images, and a locating hint. |
| add_reply | ref, body, images? | Posts a reply from Claude into that thread, with images when given. |
| resolve_thread | ref | Closes the thread. The reviewer sees "Resolved by Claude". |
| reopen_thread | ref | Opens a thread Claude closed. Not one a person closed. |
| create_comment | page, selector, body, project?, title?, screenshot?, images? | Opens a new thread on a page, pinned to one element, with a screenshot and images when given. |
| attach_screenshot | ref, screenshot | Puts a screenshot on a thread Claude opened. Not on one a person opened. |
| list_projects | none | Names, slugs, origins, open and queued counts. |
| use_project | project | Makes the lists and new comments use one project for the rest of this session. |
ref accepts FLPS-003, #flps 3, the number alone, or a thread link that
ends in #FLPS-003. project is a slug; the word all removes the default
project for one call. A thread a person resolved is never returned, and cannot
be written to. A thread Claude resolved can still be read and reopened.
create_comment pins its comment with selector alone, so the selector has to
match exactly one element on the page — #pricing .cta, not div. When it
matches nothing, the thread is still in the list, with no pin on the page.
page is a path such as /pricing; the project supplies the site, and the
answer names the full url the pin will appear on.
screenshot is the absolute path of a PNG on your machine — the one Claude
saved while it looked at the page, for example with Playwright's
locator.screenshot({ path }). The package checks that it is a PNG of at most
3.5 MB and 8000 px on a side before anything is written, then uploads it to the
thread. Only a PNG passes that check, so no other kind of file can be uploaded.
If the upload fails after the comment is open, the answer says so and
attach_screenshot retries it on the same thread. attach_screenshot only
works on threads Claude opened with the same person's token: a reviewer's
screenshot is never replaced.
images is a list of up to 10 absolute paths to PNG, JPEG, GIF or WebP files
on your machine, stored in the reply or the new comment. Claude uses it mostly
for a screenshot of the page after a fix that changes what the page shows. The
package checks every file before anything is written: the type comes from the
file's bytes, not its name, and each file is at most 3.5 MB and 8000 px on a
side. If one file cannot be used, nothing is posted and the answer names that
file. Each image is uploaded with its real content type. An add_reply with
images may have an empty body; one with neither is refused.
When no project is chosen and the queue holds threads from more than one
project, list_queue answers with those projects instead of their threads:
the name, the link, the number queued, and when the last thread was queued,
newest first. Claude asks which project the session is for, then calls
use_project. The choice lives in that session only, so two sessions on two
branches each keep their own, and nothing in Flupes changes.
get_thread ranks its locating clues by how likely each one is to appear
verbatim in your source: visible text, then semantic attributes (data-testid,
id, aria-label, name, role, other data-*), then React component names,
then ancestors, then the route, then the HTML snippet. The CSS selector and the
XPath are included and labelled weak. Computed styles and device data sit under
"Current rendering": they say what to fix, not where.
If the screenshot cannot be loaded, get_thread still returns the rest of the
thread and says that the image is missing.
After the screenshot, get_thread returns the images people pasted into the
comments: the root's first, then each reply's, oldest first. Each image follows
a label such as "Image 2 — reply by Ana", and the conversation says which
images belong to which comment. One answer carries at most 10 of them, and at
most 8 MB of image data in total, the screenshot included; the markdown names
the rest as "not shown (size budget)", and an
image that cannot be loaded is named with the reason. The image type is read
from the bytes, never taken from a header alone.
Slash commands
Run this once in the repository:
npx -y @flupes/mcp skillsIt writes three Claude Code skills — .claude/skills/fix-queue/SKILL.md,
.claude/skills/fix-thread/SKILL.md and .claude/skills/review/SKILL.md — and
.claude/skills/review/RUBRIC.md beside the last one. A team commits them next
to .mcp.json.
/fix-queueworks through the queue: read each thread, look at the screenshot and the pasted images, find the source, ask you before the change, change the working tree. When the fix changes what the page shows and Claude can capture the page, the reply carries a screenshot of the result. It replies and resolves only when a thread is done; follow-ups and questions come to you, not to the thread. It ends with one line for each thread: the ref, what changed (or what is still open), the files, and the deep link./fix-thread <ref>does the same for one thread./reviewis the other half: it finds the feedback instead of answering it. Point it at a page, a link, a component, the wordall, or nothing for the work you have not shipped yet. It judges that surface againstRUBRIC.md— accessibility, layout, type, colour, shape, states, motion, words, and what the project's own design files already decided — and files what it finds as comments pinned on the live page, each with a picture of the element. Send the ones you agree with back to Claude and/fix-queueworks them. It changes no file.
RUBRIC.md is the bar, and only /review reads it. Your own rules win over it:
it looks for CLAUDE.md, AGENTS.md, a design document, a tokens or theme file
and the design references in your component headers, and a finding that
contradicts one of those is not reported. Edit the file to change the bar for
your team; --force overwrites it.
A pin is drawn on the project's own site, so /review checks a selector and
takes its picture on that origin. Anything it cannot reach there — a story, a
page behind a sign-in, a problem that lives only in a file, work that is not
deployed — is reported in the session instead, with the reason.
A file that is already there is kept. Add --force to replace it.
After you update the package, re-run it with --force and commit the result.
A SKILL.md written by an older version carries that version's rules: one from
0.6.0 or earlier tells Claude it cannot reply to a thread or resolve it, and
get_thread no longer resolves anything — so /fix-queue would read every
thread, change the code, and leave the whole queue open. One from 0.7.0 or
earlier changes the code without asking you first, and posts its questions and
skip reasons in the thread. One from 0.10.0 or earlier does not look at the
images people pasted into a thread, and does not attach a screenshot of the
result to its reply: a team that committed the skill files before 0.11.0 must
run npx -y @flupes/mcp skills --force and commit the result to get that rule.
A skill with the same name in ~/.claude/skills wins over the one in the
repository.
Without the skills, the server ships the same prompts. Claude Code lists them as
/flupes:fix-queue, /flupes:fix-thread and /flupes:review.
When Claude Code starts
The first session after you install the server tells Claude to check the
queue once.
If the queue spans several projects, Claude first lists those projects, newest
queue first, and asks which one this session is for.
If threads are queued, Claude lists them (the ref, the first line, the route)
and asks whether you want to start: /fix-queue for all of them, and
/fix-thread <ref> for one.
Claude does not open or fix a thread until you run one of those.
An empty queue is not mentioned.
After that check, no session calls Flupes on its own: not when it starts, and
not for each message you send. Claude calls it when you ask about the feedback
or run /fix-queue or /fix-thread. The check counts as done when list_queue
answers; a failed call leaves it for the next session.
The server records it in ~/.config/flupes/greeted/ (or
$XDG_CONFIG_HOME/flupes/greeted/), one file per token and folder, with no
token in it. A new token checks once more; delete the folder to check again.
Configuration
| Name | Required | Meaning |
|---|---|---|
| FLUPES_TOKEN | yes | The personal token, flp_…. |
| --project <slug> or FLUPES_PROJECT | no | Default project filter. With one, Claude never asks which project. The flag wins over the variable; use_project wins over both; a tool argument wins over all three. |
| FLUPES_SUPABASE_URL | no | Overrides the compiled-in Flupes API address. |
| FLUPES_SUPABASE_ANON_KEY | no | Overrides the compiled-in publishable key. |
| FLUPES_DASHBOARD_URL | no | Base of the thread links. Default https://flupes.app. |
The server writes logs to stderr only. stdout carries the MCP protocol.
Errors
Each failure is one plain sentence with the next step: no FLUPES_TOKEN; a
token Flupes does not accept (create a new one in Settings → MCP); an
organization that is not on Pro; a thread that does not exist,
was resolved by a person, or is not visible to the token; a network failure.
A thread get_thread resolved itself can still be read again.
Local development
npm --prefix mcp install
npm --prefix mcp test
npm --prefix mcp run typecheck
npm --prefix mcp run buildRun the local build against the local Supabase stack. supabase status prints
the API URL and the anon key.
claude mcp add flupes-local \
-e FLUPES_TOKEN=<your token> \
-e FLUPES_SUPABASE_URL=http://127.0.0.1:54321 \
-e FLUPES_SUPABASE_ANON_KEY=<anon key from supabase status> \
-e FLUPES_DASHBOARD_URL=http://localhost:5174 \
-- node /absolute/path/to/mcp/dist/index.js --project <slug>The package imports two files directly by relative path from the product
(src/lib/comment-rows.ts, src/lib/comment-format.ts), and
comment-format.ts in turn pulls in src/lib/mentions.ts, so all three ship
inside dist/index.js. It must never import from src/extension/, which
needs import.meta.env and chrome.*. test/bundle.test.ts fails if either
one reaches the bundle.
Publishing
Publishing is a manual step that the repository owner confirms. The @flupes
scope is an npm organization; publish from an account that belongs to it. Run
npm publish from mcp/: it runs the type check, the tests and the build
first (prepublishOnly).
License
MIT — see LICENSE.
