Browse the docs

MCP reference

Eleven tools, one HTTP endpoint, standard Bearer auth. Your agent reads pins, opens their screenshots, and writes the result back onto the same pin.

Endpoint and auth

The server speaks Streamable HTTP. Every call is a JSON-RPC POST to one URL, authorised with the agent key from the extension's Connect AI agent screen:

POST https://makhtriiptwkicmksbdr.supabase.co/functions/v1/mcp Authorization: Bearer fbt_your_key

Keys look like fbt_ followed by twelve hex characters. In MCP client configs the server registers under the name design-review; it identifies itself as feedback-tool in serverInfo. It advertises tools only — there are no MCP resources or prompts.

A missing or unknown key is rejected before any tool runs, with HTTP 401 and {"error":"Invalid or missing agent key"}. Protocol-level problems keep their JSON-RPC codes — a malformed body, an unknown method, an unknown tool name. But everything a tool itself can hit — a review that doesn't exist, invalid input, a failed publish — comes back as ordinary text in a successful response. Your agent has to read the text, not just check the status.

What a key can see depends on its kind. A personal key sees the reviews it owns plus reviews on sites where its owner is a member or the owner. A per-site share key sees only that site. Anything outside that scope reads back as “not found for this agent key” rather than as a permission error.

Reading feedback

Four read-only tools. None of them pages: no limit, no offset, no cursor, and no filtering by status or severity — they return everything in scope, so on a busy account the response can be long.

list_reviews

list_reviews()

Takes no arguments at all. Returns a Markdown list of every review in scope, newest first: id, page URL, comment count and creation time. No titles, no pagination.

get_latest_review

get_latest_review()

The most recent review in scope, rendered exactly like get_review below. The usual entry point when someone just said “I left you some notes”.

get_review

get_review(reviewId)

One review in full: page URL, title, the environment it was captured in (viewport and DPR, language, platform, user agent), the page's console output, and every comment on it. Console lines are filtered to errors and warnings and cut to the last ten.

Each comment carries its id, kind, CSS selector, computed styles, discussion thread, and a pointer to its screenshot. A status line appears once the comment has moved on — an open one instead carries a reminder to call resolve_comment when the fix is real. Screenshots are never inlined here; you fetch them with get_screenshot.

ParameterTypeNotes
reviewIdstringrequired; an id out of scope reads back as “not found”

get_page_bundle

get_page_bundle(targetUrl)

Every comment on one page — open, agent-resolved and accepted alike, each printed with its status — grouped by severity in the order Blocker, High, Medium, Low, Redesign. Richer than get_review: each comment also carries the element's outerHTML in a fenced block. This is the tool to call before an audit, so you don't file a finding someone already pinned.

ParameterTypeNotes
targetUrlstringrequired; matched as an exact string — no normalisation of query, hash or trailing slash

Because the match is literal, /pricing and /pricing/ are two different pages here. Take the URL from a review rather than typing it by hand.

Screenshots and frames

These two tools return images inline — and they are the only ones that do. Opening an image also takes the pin on: the server records that your key looked at it, and reminds you about anything you left open when you next resolve or reply. The reminder expires twelve hours after the claim, and it's recorded before the image is actually delivered — a failed download still counts as taken on.

get_screenshot

get_screenshot(reviewId, commentId, variant?)

ParameterTypeNotes
reviewIdstringrequired
commentIdstringrequired
variantstringelement (default) or context; anything else is treated as element

Returns the PNG inline. A comment without a screenshot, or one outside your scope, answers “No screenshot for this comment.” in text.

get_frame

get_frame(reviewId, commentId, index)

One sampled frame of a comment's screen recording — the video a reviewer recorded in the extension. Walkthrough frames are a different thing entirely and are not served here.

The frames a comment has are listed in get_review and get_page_bundle output; there is no separate listing tool. Mind the offset: that listing counts from one, while index counts from zero, so “frame 1” is index: 0. An index that doesn't exist answers “No such frame.”

Closing the loop

Both writers set the comment's status to “resolved by agent”, which is what the reviewer sees on the pin and on the board — where they can accept it or reopen it. Accepting and reopening are theirs alone, and no MCP tool moves a comment back to open or deletes it.

resolve_comment

resolve_comment(reviewId, commentId, note)

Marks the comment resolved and attaches your note to it. The note shows in a small popover on the pin, so one or two sentences — what changed, plus a commit reference — is the right size. Detail belongs in the commit.

reply_comment

reply_comment(reviewId, commentId, body)

Posts into the pin's discussion thread and resolves it — there is no reply-without-resolving. The body is trimmed and silently cut at 2000 characters.

After either call, if you still have pins you opened but never closed, the response ends with a list of them. When nothing is outstanding it says so instead.

Posting your own findings

create_comments

create_comments(page_url, findings[], page_title?)

Your agent's own audit results, posted as pins the team sees on the page and on the board with an AI-author badge. Call get_page_bundle first: nothing stops you from filing a duplicate of a comment that's already open on the same selector.

Each finding becomes its own review holding a single comment — this is not a batch into one review.

FieldTypeNotes
page_urlstringrequired; must be http(s) and include a host
page_titlestringoptional
findingsarrayrequired; 1 to 20 per call
findings[].bodystringrequired; cut at 2000 characters
findings[].severitystringrequired; Low, Medium, High, Blocker or Redesign
findings[].selectorstringoptional; the preferred anchor
findings[].rectobjectoptional; x, y, width, height
findings[].pinobjectoptional; x, y
findings[].htmlstringoptional; kept only when the finding has a selector; cut at 2000 characters
findings[].screenshot_base64stringoptional; raw base64 PNG, no data: prefix, ~2 MB each and ~10 MB per call
findings[].consolearrayoptional; up to 10 lines of 500 characters

A finding with console is page-level and cannot also carry selector, rect, pin or html — the call is rejected if it does. A finding with no selector at all becomes a region pin, and its html is dropped. Console lines are always stored at error level. Screenshots must be complete PNGs; a truncated file is rejected outright.

Failures are reported in text and tell you whether to retry: if the anchor insert fails, the pins were created and retrying would duplicate them.

Walkthroughs

A walkthrough is a click-through replay you record to show a teammate a flow — not a code fix. It only ever shows: a viewer cannot leave a note on a frame. The text on a frame is the author's — the step's caption, plus the summary line attached at publish time. See the CLI reference for the capture itself.

record_walkthrough

record_walkthrough(goal, origin, steps[], name?)

Writes nothing and captures nothing. It turns the steps you describe into a plan file and hands back the exact local command to run — because the capture drives a real browser, which a cloud server cannot do.

FieldTypeNotes
goalstringrequired; becomes the title
originstringrequired; where the app runs, e.g. http://localhost:3000
stepsarrayrequired; at least one
steps[].actionstringrequired; open, click, type, check or scroll
steps[].targetstringrequired; a path or a selector
steps[].captionstringrequired; what this step shows
steps[].textstringrequired for type
namestringoptional; plan filename, which is also the walkthrough's identity

The command it returns has your agent key printed in it — treat that output like a credential.

publish_walkthrough

publish_walkthrough(walkthroughId, summary[])

Publishes a draft captured by the CLI and attaches the “what changed” summary written after looking at the frames. This one runs server-side — no browser needed. Re-publishing an already-public walkthrough updates its summary in place, keeping the same link.

FieldTypeNotes
walkthroughIdstringrequired; the wlk_… id the CLI prints
summaryarrayrequired; items of { stepIdx, text }, stepIdx zero-based

The summary is all-or-nothing: an empty array is fine, but a non-empty one must cover every step, exactly once. A walkthrough recorded under a different key cannot be published with yours.

What the API does not do

Worth knowing before you design around it:

No paging or filtering

No tool takes a limit, offset, cursor, status or severity filter. You get everything in scope and filter it yourself.

No unresolve, accept or delete

Over MCP the agent can only mark work resolved. Accepting and reopening belong to the people who left the feedback.

No reply without resolving

reply_comment always closes the comment as well.

No inline images outside the two image tools

Reviews and bundles never inline a screenshot — they point at get_screenshot. Recordings and video frames do come through as public storage URLs in the text.

Few error codes to branch on

Problems a tool hits arrive as plain text in a 200 response. Real HTTP and JSON-RPC errors are reserved for the envelope: a bad key, a non-POST request, a malformed body, an unknown method or tool name.

The pin guard

The same URL serves two plain HTTP routes that are not MCP tools. GET /mcp/pending returns the pins your key opened and left open — an empty body means nothing is outstanding, and it answers with at most fifty. GET /mcp/hook?client=claude|cursor|codex returns a ready Stop-hook config with your key already in it, to merge into your agent's settings next to any hooks already there.

The point is that an agent cannot quietly forget a pin it looked at: before the turn ends, the hook hands it back. It fails open, and it gives up after three pushes in a row — the counter resets as soon as nothing is outstanding, and is forgotten after fifteen minutes of quiet.

Calling /pending once marks the guard as installed for that key, which is what stops the server from offering it again in its startup instructions.