@polygonlabs/primer-mcp
v1.0.0
Published
MCP server + device-flow login for Primer — lets Claude Code (or any MCP-capable agent) publish HTML/Markdown artifacts and pull reviewer comments.
Readme
@polygonlabs/primer-mcp
Stdio MCP server for Primer — exposes Primer documents, versions, and comments as tools, and bundles a device-authorization login flow for both in-agent and CLI use. Used by Claude Code (and any other MCP-compatible client) via stdio.
Install in Claude Code
Preferred — via the Primer plugin marketplace (also installs the primer skill that drives this MCP):
/plugin marketplace add 0xPolygon/primer
/plugin install primer@primer
/reload-pluginsThe plugin bakes the prod PRIMER_API_URL into the MCP server registration, so there's nothing else to configure — open a Claude Code session and say "publish this to Primer" to trigger the device-login + create-document flow.
Manual install (skip the plugin, register the MCP yourself):
claude mcp add primer \
--env PRIMER_API_URL=https://primer-polygon.up.railway.app \
-- npx -y @polygonlabs/[email protected]Pin the version so the local install doesn't drift on each npx run.
Local dev (point at a locally-running API):
claude mcp add primer \
--env PRIMER_API_URL=http://localhost:3000 \
-- node /abs/path/to/apps/mcp/dist/index.jsLogging in
Primer uses the OAuth 2.0 device authorization flow. Two entry points, sharing one token store.
In-agent (preferred)
The Primer skill calls primer_login, which returns a deviceCode,
userCode, and verificationUri. The skill prints them to you in this
exact shape (see the design spec, Section 5):
Open this URL in your browser:
https://primer.example.com/device
Then type this code on the page:
WXYZ-1234You open the URL, type the code from your terminal into the page
(the page does not pre-fill it from the URL — this is an intentional
phishing mitigation, see design spec, Section 6), and approve.
The skill then long-polls primer_login_status until the server
returns approved; the token is written to your local store, never
shown to the agent or printed in the terminal.
CLI
If you want to log in without an agent in the loop (e.g. before a scripted session), run the same flow from the shell:
node apps/mcp/dist/index.js login
# or, after install/publish:
primer-mcp loginThe CLI prints the same verificationUri + userCode pair, polls
quietly (with a Still waiting... heartbeat every ~15s during long
waits), and exits 0 once you approve. On denied or expired it
exits 1 with a short message.
The CLI never auto-opens a browser and never echoes the bare token.
Environment variables
| Var | Required | Purpose |
|---|---|---|
| PRIMER_API_URL | yes | Base URL of the Primer API, e.g. http://localhost:3000 or https://primer.example.com. Used both to call the API and as the token-store key. |
| PRIMER_API_TOKEN | no | If set, the server uses this token verbatim and skips the store entirely. Intended for CI / impersonation. Takes precedence over the store. |
| PRIMER_TOKEN_FILE | no | Relocate the token store away from the default path. Same JSON shape, same 0600 permission requirements. |
Token store
@polygonlabs/primer-mcp writes tokens to ~/.config/primer/tokens.json
(override with PRIMER_TOKEN_FILE). The file is a plain JSON object
keyed by normalized PRIMER_API_URL, so one store can hold tokens for
local-dev, staging, and prod simultaneously without collision.
Each entry holds { tokenId, token, prefix, savedAt }. Writes are
atomic — temp file with mode 0600, fsync, rename — so an
in-agent primer_login_status and a primer-mcp login running side by
side can't tear a write.
Token lookup precedence on every tool call:
PRIMER_API_TOKENenv var- Store entry for the current
PRIMER_API_URL - No token → tools return
code: 'not_authenticated'.
primer_logout revokes the server-side token (DELETE /api/tokens/:id)
and removes the local entry. Idempotent.
Tools
| Tool | Purpose |
|---|---|
| primer_login | Start a device-authorization login. Returns deviceCode, userCode, verificationUri, expiresIn, interval. |
| primer_login_status | Long-poll the device flow. Returns { status: 'approved' \| 'pending' \| 'denied' \| 'expired' }. On approved the token is written to the store; the agent never sees it. |
| primer_logout | Revoke + clear the stored token for the current PRIMER_API_URL. |
| primer_whoami | Return the authenticated Primer user (id, email, displayName). |
| primer_create_document | Create a new document with an initial version. |
| primer_add_version | Upload a new version of an existing document. |
| primer_get_document | Fetch a document + its latest-version metadata. |
| primer_list_versions | List all versions of a document, newest first. |
| primer_get_comments | Fetch comment threads on a version (or the latest version of a document) as a markdown summary. |
Inline content (passed via primer_create_document / primer_add_version)
is capped at 10 MB per upload. Larger payloads will be rejected by
the API with code: 'bad_request'.
Error code taxonomy
Every tool error returns { code, message } JSON in content[0].text
with isError: true. Codes are machine-checkable; skills should branch
on code, never on message text.
| Code | Meaning / fix |
|---|---|
| not_authenticated | No token, or the token was rejected. Run primer_login. |
| forbidden | The token is valid but the document isn't yours. |
| not_found | The id is stale or never existed. |
| bad_request | Validation failure, payload too large, wrong format, or any non-2xx the client couldn't classify. |
| rate_limited | Server returned 429. Back off and retry. |
| network | Transport-layer failure (DNS, connection refused, TLS). Transient — retry. |
| login_denied / login_expired | Surfaced by primer_login_status as a status value (not a thrown error). Start a fresh login. |
Manual smoke (optional)
To verify the bundle locally without a real Primer instance:
pnpm --filter @polygonlabs/primer-mcp build
node --check apps/mcp/dist/index.js
PRIMER_API_URL= node apps/mcp/dist/index.js # should exit 1 with "PRIMER_API_URL is required."To exercise the full device flow, point the CLI at a running API:
PRIMER_API_URL=http://localhost:3000 node apps/mcp/dist/index.js loginPublishing to npm (maintainers only)
Releases are cut manually from a maintainer's machine with npm 2FA — no automation token, no CI publish workflow. This keeps the publish path narrowly under human + hardware-MFA control.
One-time setup:
- npm account is a member of the
@polygonlabsnpm org with publish rights. - npm 2FA enabled (
npm profile enable-2fa auth-and-writes). - Locally signed in:
npm whoamireturns your username.
Per release:
# From repo root, on a clean master with the version bump merged
git checkout master && git pull --ff-only
pnpm install --frozen-lockfile
# Verify
pnpm --filter @polygonlabs/primer-mcp typecheck
pnpm --filter @polygonlabs/primer-mcp test
pnpm --filter @polygonlabs/primer-mcp build # also runs as prepublishOnly
# Publish (prompts for 2FA OTP interactively)
cd apps/mcp
npm publish --access public
# Confirm
npm view @polygonlabs/primer-mcp versionAfter publishing, bump the version pin in plugins/primer/.claude-plugin/plugin.json (mcpServers.primer.args → ["-y", "@polygonlabs/primer-mcp@<new-version>"]) so the marketplace plugin picks up the release on the next /plugin marketplace update primer.
Optionally tag the release for traceability (no automation hangs off it):
git tag mcp-v<version>
git push origin mcp-v<version>