Integrations
MCP server
Pagr is a remote MCP server, so Claude, ChatGPT and Codex can call the same authorised tools your iMessage thread does. Same user, device, entitlement and approval gates — a different interface, not a way around them.
What it is#
A Streamable HTTP MCP server, stateless: a fresh transport and server per request, bound to the authenticated principal, with no session state held in the API process. It exposes a curated, annotated subset of the same tool registry the message router uses.
Arguments are opaque Pagr ids — proj_…, ses_…, apr_… — never filesystem paths. Results carry user-safe fields only: preview hashes, provider request ids, provider session ids, public keys and repository hints are stripped before anything is returned.
Deliberately not exposed: shell execution, arbitrary file read or write, arbitrary commands, the settings tools, cancel_workflow, and the two iMessage-turn tools (reply and ask_clarification).
The endpoint#
https://api.pagr.dev/mcpAuthenticate with Authorization: Bearer pagr_…. An unauthenticated request gets a 401 carrying the pointer a first-time client follows to discover where to authorise:
WWW-Authenticate: Bearer realm="pagr", scope="read control approve", resource_metadata="https://api.pagr.dev/.well-known/oauth-protected-resource"There is also an unauthenticated health probe at /mcp/health, which reports the service name, version, transport, how many tools are exposed, and where the protected-resource document lives. It reveals nothing about any account.
Connecting before you have set anything up#
Plenty of people meet Pagr inside an assistant rather than on this site: they connect the server, call a tool, and have no bridge installed and no Mac paired. An empty array would be a correct and useless answer, so the tools do not give one.
When a call cannot return anything useful because the account is not finished, the result carries an extra setup object alongside the normal fields — never instead of them, so a client that ignores it still behaves correctly. It names the single blocking thing (no_device, no_project, no_agent, entitlement_inactive, device_offline), says in one line what Pagr is, and gives the commands that fix it:
{ "devices": [], "setup": { "status": "setup_required", "blocker": "no_device", "what_is_pagr": "Pagr connects this conversation to the Claude Code and Codex sessions running on your own Mac…", "next_steps": [ { "do": "Install the Pagr bridge on your Mac.", "command": "npm i -g @pagr/cli" }, { "do": "Pair that Mac with your account.", "command": "pagr connect" } ], "dashboard_url": "https://pagr.dev/app" }}The same block is attached to failures that a half-finished account produces — not_found for a project that was never registered, for instance — so the answer explains the account rather than the argument. Read-only tools never raise an entitlement blocker: a lapsed subscriber still gets the truth about their Macs. The web equivalent of this path is /from-claude and /from-chatgpt.
Demo mode for reviewers#
A directory reviewer evaluating Pagr has no Mac to pair, which makes the read tools impossible to judge. Setting PAGR_MCP_DEMO_TOKEN enables one bearer token that resolves to a synthetic, read-only principal served entirely from fixed sample data.
- It is matched by constant-time hash comparison before the database is consulted, and never resolves to a real account.
- It carries the
readscope only, so every control and approve tool refuses it through the ordinary scope check — there is no demo-specific bypass. - Demo calls never reach the tool registry, the command dispatcher, or any database, so nothing can be read from or written to a real account.
- Every record returned is labelled
"demo": truewith a notice and sample-data device names, so it cannot be relayed as somebody’s real machine.
Scopes#
Three scopes, checked per tool. A denial is itself an audited event.
| Scope | Grants |
|---|---|
read | See what is happening: your Macs, projects, agent sessions and pending approvals. Read-only. |
control | Start, instruct and stop Claude Code or Codex sessions, and create review workflows. |
approve | Allow or deny what an agent is waiting on, high-risk actions included. If the user has switched off Approve high-risk actions by text, a Tier C allow comes back as a step-up link instead of approving. |
Minting a personal token#
There is one credential type and two ways to get it; both are rows in the same table. Only the sha256 of a token is stored, the plaintext is shown exactly once, every lookup bumps last_used_at, and revoked or expired tokens are refused.
In the dashboard, go to Security → API access, name the token, tick the scopes it needs, and copy it. Personal tokens have no expiry; revoke them from the same page.
claude mcp add --transport http pagr https://api.pagr.dev/mcp \ --header "Authorization: Bearer pagr_…"OAuth 2.1 with PKCE#
For clients that would rather not handle a pasted token, Pagr is its own authorization server as well as its own resource server — which the MCP authorization specification permits. Access tokens last 90 days; refresh tokens last 30 and rotate on every use.
| Endpoint | What it is |
|---|---|
GET /.well-known/oauth-protected-resource/mcp | RFC 9728 protected-resource metadata. Also served at the root path. |
GET /.well-known/oauth-authorization-server | RFC 8414 metadata, advertising code_challenge_methods_supported: ["S256"]. |
POST /oauth/register | RFC 7591 dynamic client registration. HTTPS or HTTP-loopback redirect URIs only, rate-limited. |
GET /oauth/authorize | Validates the request, then redirects to the web consent screen. |
POST /oauth/authorize/decision | Session-authenticated; mints the single-use authorization code. |
POST /oauth/token | authorization_code with PKCE S256 verified, and refresh_token. |
POST /oauth/revoke | RFC 7009 revocation. |
What is enforced#
- S256 only —
plainis refused. - Authorization codes are single-use, valid for at most ten minutes, and consumed by an atomic update. Replaying one revokes the access token the first exchange minted.
- Reusing a refresh token revokes every grant that client holds for that user.
client_idandredirect_urierrors are answered with a400, never a redirect.- The RFC 8707
resourceparameter is validated against the MCP endpoint, and the RFC 9207issparameter rides every authorization response. - The consent screen names the application and the exact scopes it is asking for.
The endpoint and the discovery documents answer cross-origin requests from any origin, with credentials off, so a browser-hosted MCP client can read them and can see the WWW-Authenticate challenge it needs in order to start authorising. The dashboard API keeps its own, pinned, credentialed policy.
Tools#
| Tool | Scope | Destructive | What it does |
|---|---|---|---|
list_devices | read | no | Paired Macs, online state, and which agents are connected on each. |
list_projects | read | no | Registered projects with aliases and the Mac they live on. Never returns paths. |
list_agent_sessions | read | no | Sessions, active first, with project, provider, status and task summary. |
get_agent_status | read | no | One session's status plus its most recent events. |
list_pending_approvals | read | no | Approvals waiting on the user, with risk tier and preview. |
start_agent_session | control | no | Start Claude Code or Codex on a registered project with an instruction. |
send_agent_instruction | control | no | Send a follow-up to an existing session. |
start_review_workflow | control | no | Create an implement-then-review or review-fix-loop workflow. |
stop_agent_session | control | yes | Interrupt a running session mid-task. |
respond_to_approval | approve | yes | Allow or deny one pending approval, Tier C included — unless the user has turned off text approval for high-risk actions, which sends back a step-up link instead. |
Each tool's input schema is the registry tool's own schema. Results are returned as JSON text plus structured content; a failure sets isError with an error code and a message.
Approvals over MCP#
respond_to_approval behaves exactly as it does over iMessage, because it is the same registry tool behind both. A Tier C approval — production, secrets, a protected-branch push, something destructive or cost-bearing — is decided here like a Tier B one, bound to the same request and preview hash and audited with its tier.
The one exception is the user's own opt-out. With high_risk_remote_enabled turned off in their settings, a Tier C allow does not approve; it returns:
{ "status": "step_up_required", "message": "… /approve/<token> …" }The message contains a signed web link the user has to open and re-authenticate on. A Tier C deny is never gated that way. See approvals.
Audit#
Every call writes an audit row — including the ones that were denied for lack of a scope — with the actor, the action (mcp.tool_call or mcp.tool_denied) and the calling surface recorded as mcp. That is how a tool call made from Claude Desktop stays distinguishable from the same action taken by text.