Skip to content
Pagr

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/mcp

Authenticate 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 read scope 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": true with 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.

MCP scopes
ScopeGrants
readSee what is happening: your Macs, projects, agent sessions and pending approvals. Read-only.
controlStart, instruct and stop Claude Code or Codex sessions, and create review workflows.
approveAllow 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.

OAuth endpoints
EndpointWhat it is
GET /.well-known/oauth-protected-resource/mcpRFC 9728 protected-resource metadata. Also served at the root path.
GET /.well-known/oauth-authorization-serverRFC 8414 metadata, advertising code_challenge_methods_supported: ["S256"].
POST /oauth/registerRFC 7591 dynamic client registration. HTTPS or HTTP-loopback redirect URIs only, rate-limited.
GET /oauth/authorizeValidates the request, then redirects to the web consent screen.
POST /oauth/authorize/decisionSession-authenticated; mints the single-use authorization code.
POST /oauth/tokenauthorization_code with PKCE S256 verified, and refresh_token.
POST /oauth/revokeRFC 7009 revocation.

What is enforced#

  • S256 only — plain is 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_id and redirect_uri errors are answered with a 400, never a redirect.
  • The RFC 8707 resource parameter is validated against the MCP endpoint, and the RFC 9207 iss parameter 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#

MCP tools
ToolScopeDestructiveWhat it does
list_devicesreadnoPaired Macs, online state, and which agents are connected on each.
list_projectsreadnoRegistered projects with aliases and the Mac they live on. Never returns paths.
list_agent_sessionsreadnoSessions, active first, with project, provider, status and task summary.
get_agent_statusreadnoOne session's status plus its most recent events.
list_pending_approvalsreadnoApprovals waiting on the user, with risk tier and preview.
start_agent_sessioncontrolnoStart Claude Code or Codex on a registered project with an instruction.
send_agent_instructioncontrolnoSend a follow-up to an existing session.
start_review_workflowcontrolnoCreate an implement-then-review or review-fix-loop workflow.
stop_agent_sessioncontrolyesInterrupt a running session mid-task.
respond_to_approvalapproveyesAllow 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.