Skip to content

The MCP endpoint

POST /mcp is a Model Context Protocol endpoint. An agent authenticates with a project-scoped API token and calls tools that create documents, revise them, and read back what humans said — so a long-form plan gets considered review instead of scrolling past in a terminal.

Getting a token

Mint one from the project: /projects/{id}/mcp-token, and /projects/{id}/mcp-token/regenerate to roll it. Tokens are stored hashed and scoped — an MCP token is not a site-review token and cannot be used as one. Any token can be revoked from /account/api-tokens/{tokenId}/revoke.

The first-run wizard mints one for you at /welcome/connect.

The Claude Code plugin

Use this for the skills, not for the endpoint. A plugin holds one set of config values per user — Claude Code deliberately refuses to read them from a repository’s settings, so a clone cannot inject a credential — which means one api_token across every project you work in. Loupe’s tokens are per project, so anyone with more than one wants the claude mcp add line above and the plugin alongside it for the skills.

For a single project the plugin does both at once:

Terminal window
claude plugin marketplace add ubermuda/loupe
claude plugin install loupe@loupe

Installing asks for a project API token, from the project’s Connect page. The endpoint defaults to the hosted instance; if you self-host, set it to your own /mcp URL, which the same page shows. The token is marked sensitive, so it goes to the OS keychain rather than a settings file, and it is never read from a repository’s .claude/settings.json: a cloned project cannot inject one.

Setting and changing the two values

Answer the prompts, or pass them on the command line:

Terminal window
claude plugin install loupe@loupe \
--config server_url=https://loupe.example.com/mcp \
--config api_token=<token>

To change either one later — pointing at a different instance, switching projects, or rotating a token — run /plugin configure loupe@loupe inside Claude Code, or re-run the same install --config command with the new value. Re-running prints Plugin "loupe@loupe" is already installed, which reads like nothing happened; the config is updated regardless. Confirm with claude mcp list, which prints the endpoint the plugin resolved:

plugin:loupe:loupe: https://loupe.example.com/mcp (HTTP) - ✔ Connected

Omitting a value leaves the notice 1 userConfig option not yet set after install. For server_url that is cosmetic — the default applies — but the plugin cannot work until a token is set.

Alongside the server the plugin ships loupe:loupe-documents, which formats a document for the review UI, and loupe:loupe-site-review, which works the comment loop. The claude mcp add one-liner on the Connect page remains the right choice for any other MCP client.

A hand-configured server of the same name wins. If you previously ran claude mcp add ... loupe ..., that entry takes precedence and the plugin’s server is ignored with no warning — claude mcp list shows loupe rather than plugin:loupe:loupe.

Whether that is a problem depends on how many projects you have. With one, it is a leftover: claude mcp remove loupe and let the plugin serve the endpoint. With several it is the arrangement you want — the per-project server carries that project’s token while the plugin supplies the skills — and removing it would point every project at whichever single token the plugin holds.

Working across several projects

Register the server per project and keep the plugin for the skills. claude mcp add defaults to local scope, which is per project and stays out of the repository; do not pass --scope project, which writes the token into a committed .mcp.json.

To keep the token out of the file entirely, .mcp.json interpolates the environment, so the value can live in the project’s own untracked settings:

{
"mcpServers": {
"loupe": {
"type": "http",
"url": "https://loupe.example.com/mcp",
"headers": { "Authorization": "Bearer ${LOUPE_API_TOKEN}" }
}
}
}

with LOUPE_API_TOKEN set under env in .claude/settings.local.json.

What the tools do

Roughly in the order an agent uses them:

ToolPurpose
document_createSubmit Markdown as a new document; returns a review URL
document_reviseSubmit a new version, described by what changed
document_get / document_listRead a document, or enumerate the project’s
document_get_reviewVerdict, threaded comments, and answered decision blocks
document_reply_to_commentReply to a reviewer’s thread
document_mark_comment_addressedMark a thread acted on
document_highlightTint the passages to read first (off by default — see below)
document_renameChange the title without minting a version
document_archive / document_unarchiveTake it out of the listing, or put it back
document_set_tags / document_set_referencesGroup it, or link it to sibling documents
tag_listThe project’s existing tag vocabulary
site_review_getComments submitted through the widget
site_review_mark_comment_addressedMark a widget comment acted on, so the next site_review_get skips it

Two things agents get wrong

Comment ids do not survive document_revise. Re-anchoring copies a comment onto the new version and leaves the original behind, so replying after revising writes into rows nobody reads. Reply first, then revise — or revise, then re-read the review for fresh ids. See Documents and review.

A decision block’s id is permanent. Rewording its options is safe; changing its id silently discards the reviewer’s answer.

Configuration

document_highlight is behind the review.highlights.enabled feature flag, seeded off. An agent tinting the passages a human should read first steers the review, which is a nudge an operator opts into rather than inherits. While it is off the tool is absent from tools/list and from the Connect page, so an agent never learns of a tool this instance would refuse — switch it on in Admin → Feature flags. A client holding a tool list from before the flag changed and calling it anyway gets a plain refusal, not a broken call.

MCP_ALLOWED_HOSTS is a DNS-rebinding allowlist — hostnames only, no port. It must contain the hostname agents actually use, or every call is rejected with a 403 that names the variable and echoes the host it rejected. See Environment variables.

An unauthenticated POST /mcp answers 401, not 404. A 404 means the route did not register; a 403 is the rebinding guard.