@paigy/mcp
Canonical setup: curl -fsSL https://paigy.ai/install | sh — one command, one
QR scan; no pairing codes. Everything below is the manual per-client reference for
machines that can't run the harness.
The AI agent harness that calls you. This MCP server lets an agent notify a user and await their reply — so a long-running agent can ask a question, hand off, and resume on the answer. It's a thin MCP-tool wrapper over @paigy/sdk (packages/sdk) — use the SDK directly from any Node process that isn't an MCP client.
Install
As a Claude Code plugin (one step)
/plugin marketplace add paigy-ai/mcp
/plugin install paigy
The Paigy MCP connects automatically. On the first Paigy interaction while
unpaired, the agent shows a pairing code; approve it in Paigy. /paigy-onboard
remains the terminal-only fallback.
[!NOTE]
If you are installing the plugin inside an active Claude Code session, you must type /reload-plugins (or restart the session) afterward so the terminal client starts the MCP server and exposes the new tools to the agent.
Or add the MCP directly
No clone needed. Add it to Claude Code (-s user = available in every project; drop it for just the current one):
claude mcp add paigy -s user -- npx -y @paigy/mcp@latest
Or wire it into any MCP client config:
{
"mcpServers": {
"paigy": {
"command": "npx",
"args": ["-y", "@paigy/mcp@latest"]
}
}
}
First-time pairing (link the server to your Paigy account):
npx -p @paigy/mcp@latest paigy-mcp-onboard
It talks to the hosted backend by default — no config needed. Set
PAIGY_BACKEND_URL=http://localhost:3000 only for local development.
Codex CLI (OpenAI)
Recommended: install the Paigy Codex plugin (MCP configuration plus the Paigy workflow skill):
codex plugin marketplace add paigy-ai/mcp --ref main
codex plugin add paigy@paigy-ai
Then pair your phone — under the same agent name the plugin's MCP runs as
(the token is saved per agent; a mismatched name leaves Codex "not paired"
against an approved pairing):
PAIGY_AGENT=codex npx -y -p @paigy/mcp@latest paigy-mcp-onboard
Or add the MCP server directly (writes ~/.codex/config.toml):
codex mcp add paigy --env PAIGY_AGENT=codex -- npx -y @paigy/mcp@latest
Or add the entry to ~/.codex/config.toml by hand:
[mcp_servers.paigy]
command = "npx"
args = ["-y", "@paigy/mcp@latest"]
env = { PAIGY_AGENT = "codex" }
Then pair: PAIGY_AGENT=codex npx -p @paigy/mcp@latest paigy-mcp-onboard (or have the agent call the configure tool — it pairs under its own name automatically).
Gemini CLI
gemini mcp add -s user -e PAIGY_AGENT=gemini paigy npx -y @paigy/mcp@latest
-s user makes it available across all projects — omit it for just the current one.
Or add the entry to ~/.gemini/settings.json:
{
"mcpServers": {
"paigy": {
"command": "npx",
"args": ["-y", "@paigy/mcp@latest"],
"env": { "PAIGY_AGENT": "gemini" }
}
}
}
Then pair: PAIGY_AGENT=gemini npx -p @paigy/mcp@latest paigy-mcp-onboard.
PAIGY_AGENT names the agent AND keys its token slot (defaults to
mcp-agent). Each client reads only its own slot, so pair with the same
PAIGY_AGENT the client's config uses — and set it per client so you can
tell your connected agents apart.
Tools
One catalog, on every transport (2026-09-11): the agent tools are AGENT_TOOLS in
packages/schema/src/tools.ts — contact, manage_goals, get_goal, search, check_activity,
send_feedback — and both this server and the hosted MCP
(apps/api/src/mcp) publish that list and dispatch it through the SDK's one runTool.
This server adds only configure, the one identity tool, which pairs, reports who you are,
renames and logs this agent out (logOut: true) over the token file this machine holds, and listen.
onboard is now configure; pair and unpair are folded into it (2026-10-07). See the SDK contract for the
contact shape. Sending returns immediately unless it asks to wait: wait: true holds one
cancellable ~45-second window here for a response to what was sent (the hosted transport
returns after one read), and contact({}) holds one for anything addressed to you; after an
expired window, wait again without sending again. Every Goal a contact names must already
exist (create it with manage_goals); a question with workItBlocks asks for a call; a
question, an update or an answer is at most 250 characters with its context in units; and
withdrawQuestionIds takes back a question of yours. update_goal is gone (2026-10-07): progress
is a contact update (one the person does not need yet is kept as the Goal's progress), a review is
acknowledged with ackEventIds, and a Goal is postponed with manage_goals defer (you are woken
when its time comes). Durable Entries and accepted answers can
repeat on reads; contact with nothing to send receives what is addressed to you (a request the
user started toward you, an answer relayed to something you asked, a handoff, a question you owe) without
consuming any of it until you acknowledge it (ackEventIds), and get_goal reads a Goal.
claim_goal is gone (2026-10-07): you are on a Goal from your first write to it (a contact
naming it, or a manage_goals change), never from a read, so there is nothing to claim or join;
who_is_working is now check_activity. manage_goals creates, edits, assigns, organizes and closes work, a change at a
time. Bookkeeping (operationId, the manage request id, the revision an edit was read at) is
minted here, never asked of the model. Retired
reply-lease/ACK/work/callback tools and hidden notification aliases are rejected.
The standalone listener below is still a legacy host and target-release blocker.
Its old reply intake must migrate before enabling it against the target runtime; it is
not a compatibility fallback for target MCP tools.
Configuration
PAIGY_BACKEND_URL — the Paigy API base (defaults to the hosted backend).
HTTPS_PROXY / HTTP_PROXY / NO_PROXY — honored for every REST call, with
curl's semantics. Node's fetch ignores these (unlike curl), so the server
wires them up itself; in environments where egress only flows through a proxy
(Claude Code on the web, corporate CI), calls route through it automatically —
no extra setup. Two edges: with HTTP_PROXY set, an http://localhost backend
proxies too unless NO_PROXY=localhost (hostname entries work; CIDR ranges are
ignored), and paigy-listen's realtime wake channel is a websocket that doesn't
proxy — its REST reads do.
Wake any harness (paigy-listen)
paigy-listen is the self-hosted push daemon: it subscribes to this connection's
wake channel and reads what is waiting on every nudge. Keep it alive past the terminal
with paigy-listen --install (launchd on macOS, systemd user unit on Linux;
--uninstall removes it).
The read is receive (behind contact({})) — the open Deliveries addressed to this agent — and it
consumes nothing: no lease, no acknowledgement, and the same read twice returns the
same list. So the daemon hands the launcher what it found and stops there; it never
writes, because an agent is on a Goal from its first write and the daemon shares this
machine's token with the agent it launches: a write here would put that agent on work it
never started.
To launch an agent — any agent, not just Claude — when work arrives, set
PAIGY_ON_WAKE to a command before --install:
PAIGY_WORK | everything the wake found, as JSON (deliveries, goal) |
PAIGY_EVENT | the wake that caused this run — boot, wake:reply, wake:request… |
PAIGY_PARENT_ID | the thread to continue on (PAIGY_THREAD_ID is the same value, for existing scripts) |
PAIGY_DELIVERY_ID | the Delivery being acted on (get_goal on PAIGY_GOAL_ID reads its conversation) |
PAIGY_GOAL_ID | the Goal it belongs to — read it with get_goal first |
PAIGY_TEXT | what the person said, in prose |
Hand $PAIGY_WORK to a harness that can read JSON and decide for itself; use the
scalars for a plain shell launcher that shouldn't need jq. They describe one
item — the oldest open Delivery — because the contract is one thread at a time. An
absent fact is unset rather than empty, so ${PAIGY_GOAL_ID:-} distinguishes "no
Goal" from "an empty id".
PAIGY_ON_WAKE='claude -p "Handle the Paigy work in $PAIGY_WORK — read each Goal it names with get_goal first."' \
npx -y -p @paigy/mcp paigy-listen --install
npx -y -p @paigy/mcp@latest paigy-listen --brief
A session that starts listening (listen, #2265)
Run listen to obtain the exact background command for the current session. For Codex,
paigy-listen reads CODEX_THREAD_ID, checks that the installed codex queue supports
--thread and --message, then queues a wake into that existing conversation. It uses
argument arrays, never interpolates user text into a shell, and never starts another
conversation. Two threads in the same folder have separate listener marks and checkpoints.
Onboarding forwards the thread identifier into the MCP environment; listen forwards it
into the listener command explicitly. Re-running onboarding repairs an existing Paigy allowlist while preserving custom variables; restart the MCP server to inherit the repaired environment. An older slot-wide listener is reported as already receiving until deliberately stopped, so upgrades do not start a second subscriber.
PAIGY_ON_WAKE remains an explicit override with its original per-sweep behavior: an
existing script may ignore boot or timer events. Without a supported adapter, the listener
still prints events, but reports only receiving; a process being alive does not prove it
can resume an agent. listening with a ready adapter means the command is available, and
lastQueuedAt records its last successful exit. Neither claims that the agent read or
handled a reply. Queue errors are visible in listener output and in listen status.
Native Codex wakes coalesce unchanged backlog, including across listener restarts. The listener compares source Entry IDs and accepted Answer timestamps on the bounded list of pending review Goals; its own progress and claims are excluded. A later reply is new evidence even if its words are identical. Failed handoffs remain retryable
on the existing timer, next nudge or reconnect; discovery never claims or acknowledges
work. Queue commands are serialized, so a burst becomes one fresh discovery read after
an in-flight handoff. A command accepted just before a process crash may be queued again;
the resumed agent must still read and acknowledge actual evidence through Paigy.
A managed Paigy session carries PAIGY_HARNESS and keeps its existing delivery pump;
it does not start another listener. PAIGY_SESSION_ID binds API requests and alone does
not establish harness delivery. Other hosts need a supported wake command or a host that
actually surfaces watched-task output. Use a session listener for Codex, not --install:
the service is machine-wide and has no current conversation.
$PAIGY_TEXT is whatever the user said, expanded inside a shell command — keep it
quoted, as above. For anything longer than a one-liner, point PAIGY_ON_WAKE at a
script and branch on $PAIGY_EVENT there:
#!/bin/sh
codex exec "Paigy goal $PAIGY_GOAL_ID. Read it with get_goal first. The user said: $PAIGY_TEXT. Reply with contact when done."
Stalled work (turn-end hook — Claude Code, Codex, Gemini CLI, Antigravity)
An agent that goes quiet leaves its Goals behind. Three things hand it that work back, all
reading one rule (no progress for 3 days, COLD_AFTER_MS): contact({}) lists it under
stalled, paigy-listen carries it in PAIGY_WORK (and launches for it on boot), and this
hook holds a session's turn end once a day to list it. One program, four tools:
Claude Code (~/.claude/settings.json) — installed by the paigy plugin:
{ "hooks": { "Stop": [{ "hooks": [{ "type": "command", "command": "npx -y -p @paigy/mcp paigy-stalled" }] }] } }
Codex (~/.codex/config.toml) — its Stop event takes the same { decision, reason }, so
the same program serves it. Written for you when you pair from another tool:
[[hooks.Stop]]
[[hooks.Stop.hooks]]
type = "command"
command = "npx -y -p @paigy/mcp@latest paigy-stalled --agent codex"
timeout = 60
Gemini CLI / Antigravity (~/.gemini/settings.json, ~/.gemini/antigravity-cli/settings.json)
— the turn-end event there is AfterAgent, and the reminder also arrives as a systemMessage,
which is the field that event promises will reach somebody:
{ "hooks": { "AfterAgent": [{ "matcher": "*", "hooks": [{ "name": "paigy-stalled", "type": "command",
"command": "npx -y -p @paigy/mcp@latest paigy-stalled --agent antigravity", "timeout": 60000 }] }] } }
--agent <slot> is not optional outside Claude Code: a hook is not the MCP server, so it never
inherits the PAIGY_AGENT that server was registered with, and without the flag it reads this
session's own slot — a different identity, with no stalled work of its own. Pass the same value as
the tool's MCP registration (codex, antigravity, or whatever you set PAIGY_AGENT to).
It is silent when nothing is stalled, when the stop is already being continued by a hook, and
after it has reminded that session once today. Codex's older notify program is not a place for
this: it is spawned fire-and-forget with stdio null, so nothing it prints can reach the agent.
Statusline (Claude Code)
paigy-statusline prints which Paigy identity this Claude Code session is, for the
status bar:
paigy: Marlow
Enable it in ~/.claude/settings.json:
"statusLine": { "type": "command", "command": "npx -y -p @paigy/mcp@latest paigy-statusline" }
It reads only ~/.paigy — no network. paigy: paired · session hatches on first use means the
machine is paired and this session's identity is minted by its first tool call;
paigy: unpaired means there's no token — run /paigy-onboard to pair.
Publishing (maintainers)
Tool input schemas are generated from zod in src/schema.ts as draft-2020-12 JSON
Schema, and src/schema.test.ts guards that every tool stays valid (strict clients
like the Anthropic API reject anything else). A schema fix only reaches agents once
a new version is published to npm — 0.8.0 once shipped without a committed fix and
400'd strict clients for weeks. So after any change under src/schema* or the tool
definitions: bump the version and pnpm publish (don't rely on the commit alone).
Install recipes pin @paigy/mcp@latest so a fresh npx picks up the new version; if a
stale one sticks, rm -rf ~/.npm/_npx and restart the client.
License
MIT