@paigy/mcp
v0.28.0
Published
Paigy MCP server — a voice inbox for your AI agents. Lets an agent notify a user and await their reply.
Readme
@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.
A voice inbox for your AI agents. 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 paigyThe 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@latestOr 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-onboardIt 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-aiThen 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-onboardOr add the MCP server directly (writes ~/.codex/config.toml):
codex mcp add paigy --env PAIGY_AGENT=codex -- npx -y @paigy/mcp@latestOr 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 pair 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_AGENTnames the agent AND keys its token slot (defaults tomcp-agent). Each client reads only its own slot, so pair with the samePAIGY_AGENTthe client's config uses — and set it per client so you can tell your connected agents apart.
Tools
pair— pair this agent with the user's Paigy account (one-time). No args to start: returns the code to show the user and begins polling for approval in the background; pass the returneddevice_codeto collect the result (it returns the moment the user approves). On success it prompts you to allowlist Paigy's notify/await tools so they run without an approval prompt each time.unpair— log this agent out of the user's Paigy account; revokes the token server-side and deletes the local one.enable_tools— after pairing (and only with the user's consent), allowlist Paigy's notify/await tools so they run without an approval prompt each time. Writes Claude Code'spermissions.allow—scope:'user'(default, every project) orscope:'project'(this repo). Merges, never clobbers; leavespair/unpairhuman-approved.contact— THE way to reach the user: tell them something, or ask and get their answer. Core form is two fields —ask(plain prose: what you need to tell them or find out) andwaiting(what happens to your work meanwhile:none= just informing,soft= want an answer but can keep working,hard= stopped until answered — reaches them urgently and escalates to a real phone call). Paigy's broker picks the channel, phrasing, and answer format. When the choices themselves must be seen, attachoptions(each may carry a sandboxedhtmlorimagepreview); attachvisualsfor context screenshots. Returns{ notificationId, threadId }; a threaded follow-up supersedes that thread's pending items, and an identical threaded re-send escalates in place. If a reply comes back as{kind:'clarify', chunks:[...]}, contact again on the SAME threadId with an expanded ask. (The formernotify_user/notifynames remain accepted as hidden aliases for older setups, and the fully-shaped wire form — context/select/urgency — is still accepted from code; neither is part of the model surface anymore.)await_reply— wait for the user's reply to a specific notification (pass its notificationId). Scoped: will not return replies meant for other notifications. Returnsreply/remind/idle.check_replies— catch-up sweep: returns replies you haven't consumed yet (now marked seen) plus still-pending notifications, new user-initiated requests, and owed callbacks.set_task_state— report progress on a request:in_progress/completed/needs_input.schedule_callback— promise the user a follow-up (on_done/on_blocked/scheduled) so it's not dropped if you go idle.
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'sfetchignores 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: withHTTP_PROXYset, anhttp://localhostbackend proxies too unlessNO_PROXY=localhost(hostname entries work; CIDR ranges are ignored), andpaigy-listen's realtime wake channel is a websocket that doesn't proxy — its REST sweeps do.
Wake any harness (paigy-listen)
paigy-listen is the self-hosted push daemon: it subscribes to this connection's
wake channel and sweeps on every nudge. Keep it alive past the terminal with
paigy-listen --install (launchd on macOS, systemd user unit on Linux; --uninstall
removes it).
To launch an agent — any agent, not just Claude — when work arrives, set
PAIGY_ON_WAKE to a command before --install. The sweep has already claimed the
work, so the launcher reads it from the environment rather than calling
check_replies again:
| Variable | What it holds |
| --- | --- |
| PAIGY_WORK | the whole swept payload as JSON (replies, requests, owedCallbacks, stalled, threads) |
| PAIGY_EVENT | the wake that caused this run — boot, wake:reply, cron:callback… |
| PAIGY_THREAD_ID | the thread to continue on |
| PAIGY_NOTIFICATION_ID | the notification being answered or acted on (unset for an owed callback) |
| PAIGY_CONTEXT_THREAD_ID | a past conversation the user seeded this with — get_thread it first |
| PAIGY_TEXT | what the user said (or the callback note you owe them), 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 thread without a turn already in flight — because the queue rail's
contract is one thread at a time. An absent fact is unset rather than empty, so
${PAIGY_CONTEXT_THREAD_ID:-} distinguishes "no seed" from "seeded with nothing".
# Claude Code — hand it everything and let it plan:
PAIGY_ON_WAKE='claude -p "Handle the Paigy work in $PAIGY_WORK — rehydrate threads you do not recognize via get_thread first."' \
npx -y -p @paigy/mcp paigy-listen --install
# Codex (or any CLI harness) — the scalars are enough for a one-liner:
PAIGY_ON_WAKE='codex exec "Continue Paigy thread $PAIGY_THREAD_ID. The user said: $PAIGY_TEXT. Call get_thread on it first if you do not recognize it, then reply with contact."' \
npx -y -p @paigy/mcp paigy-listen --install$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
# ~/.paigy/on-wake.sh — chmod +x, then PAIGY_ON_WAKE=~/.paigy/on-wake.sh
seed=""
[ -n "${PAIGY_CONTEXT_THREAD_ID:-}" ] && seed="It continues thread $PAIGY_CONTEXT_THREAD_ID — get_thread that first."
case "$PAIGY_EVENT" in
cron:callback*) codex exec "You owe the user a callback on thread $PAIGY_THREAD_ID: $PAIGY_TEXT. Deliver it with contact." ;;
*) codex exec "Paigy thread $PAIGY_THREAD_ID. The user said: $PAIGY_TEXT. $seed Reply with contact when done." ;;
esacStatusline (Claude Code)
paigy-statusline prints a one-line connection status — the account's session
mode and whether a phone is paired — for Claude Code's status bar:
paigy: all calls · phone ✓Enable it in ~/.claude/settings.json:
"statusLine": { "type": "command", "command": "npx -y -p @paigy/mcp@latest paigy-statusline" }It reads GET /api/status with the pairing token and caches the result in
~/.paigy/status.json for 60 seconds, so the bar's constant refreshes never
hammer the API; when offline it shows the last known state. paigy: unpaired
means there's no token (or the pairing was revoked) — run /paigy-onboard to
pair; paigy: … means the status isn't known yet (first run while offline).
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
