@paigy/mcp 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -3
- package/dist/{chunk-XXBSXCOC.js → chunk-AILC3H3U.js} +8 -4
- package/dist/{chunk-4MRCUW5I.js → chunk-BUELEUTV.js} +46 -19
- package/dist/chunk-GU7C5H6L.js +15 -0
- package/dist/index.js +38 -11
- package/dist/listen.js +2 -1
- package/dist/onboard.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ approve).
|
|
|
20
20
|
No clone needed. Add it to Claude Code (`-s user` = available in every project; drop it for just the current one):
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
claude mcp add paigy -s user -- npx -y @paigy/mcp
|
|
23
|
+
claude mcp add paigy -s user -- npx -y @paigy/mcp@latest
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
Or wire it into any MCP client config:
|
|
@@ -30,7 +30,7 @@ Or wire it into any MCP client config:
|
|
|
30
30
|
"mcpServers": {
|
|
31
31
|
"paigy": {
|
|
32
32
|
"command": "npx",
|
|
33
|
-
"args": ["-y", "@paigy/mcp"]
|
|
33
|
+
"args": ["-y", "@paigy/mcp@latest"]
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
}
|
|
@@ -39,12 +39,57 @@ Or wire it into any MCP client config:
|
|
|
39
39
|
First-time pairing (link the server to your Paigy account):
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
npx -p @paigy/mcp paigy-mcp-onboard
|
|
42
|
+
npx -p @paigy/mcp@latest paigy-mcp-onboard
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
It talks to the hosted backend by default — no config needed. Set
|
|
46
46
|
`PAIGY_BACKEND_URL=http://localhost:3000` only for local development.
|
|
47
47
|
|
|
48
|
+
### Codex CLI (OpenAI)
|
|
49
|
+
|
|
50
|
+
Codex speaks MCP, so the same server drops in. Add it (writes `~/.codex/config.toml`):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
codex mcp add paigy --env PAIGY_AGENT=codex -- npx -y @paigy/mcp@latest
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Or add the entry to `~/.codex/config.toml` by hand:
|
|
57
|
+
|
|
58
|
+
```toml
|
|
59
|
+
[mcp_servers.paigy]
|
|
60
|
+
command = "npx"
|
|
61
|
+
args = ["-y", "@paigy/mcp@latest"]
|
|
62
|
+
env = { PAIGY_AGENT = "codex" }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Then pair: `npx -p @paigy/mcp@latest paigy-mcp-onboard` (or have the agent call the `pair` tool).
|
|
66
|
+
|
|
67
|
+
### Gemini CLI
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
gemini mcp add -s user -e PAIGY_AGENT=gemini paigy npx -y @paigy/mcp@latest
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`-s user` makes it available across all projects — omit it for just the current one.
|
|
74
|
+
Or add the entry to `~/.gemini/settings.json`:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"mcpServers": {
|
|
79
|
+
"paigy": {
|
|
80
|
+
"command": "npx",
|
|
81
|
+
"args": ["-y", "@paigy/mcp@latest"],
|
|
82
|
+
"env": { "PAIGY_AGENT": "gemini" }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Then pair: `npx -p @paigy/mcp@latest paigy-mcp-onboard`.
|
|
89
|
+
|
|
90
|
+
> `PAIGY_AGENT` just names the agent on the device-approval screen (defaults to
|
|
91
|
+
> `mcp-agent`) — set it per client so you can tell your connected agents apart.
|
|
92
|
+
|
|
48
93
|
## Tools
|
|
49
94
|
|
|
50
95
|
- **`notify_user`** — notify the user (context: title + description chunks, plus optional options — each an answerable choice that can carry a sandboxed `html` or `image` preview for visual "pick one" decisions — visuals, `urgency`, and `parentId` for a clarification). Returns a request id + thread id. If a reply comes back as `{kind:'clarify', chunks:[...]}`, respond via notify_user with the SAME threadId and an expanded description.
|
|
@@ -57,6 +102,17 @@ It talks to the hosted backend by default — no config needed. Set
|
|
|
57
102
|
|
|
58
103
|
- `PAIGY_BACKEND_URL` — the Paigy API base (defaults to the hosted backend).
|
|
59
104
|
|
|
105
|
+
## Publishing (maintainers)
|
|
106
|
+
|
|
107
|
+
Tool input schemas are generated from zod in `src/schema.ts` as draft-2020-12 JSON
|
|
108
|
+
Schema, and `src/schema.test.ts` guards that every tool stays valid (strict clients
|
|
109
|
+
like the Anthropic API reject anything else). **A schema fix only reaches agents once
|
|
110
|
+
a new version is published to npm** — 0.8.0 once shipped *without* a committed fix and
|
|
111
|
+
400'd strict clients for weeks. So after any change under `src/schema*` or the tool
|
|
112
|
+
definitions: **bump the version and `pnpm publish`** (don't rely on the commit alone).
|
|
113
|
+
Install recipes pin `@paigy/mcp@latest` so a fresh `npx` picks up the new version; if a
|
|
114
|
+
stale one sticks, `rm -rf ~/.npm/_npx` and restart the client.
|
|
115
|
+
|
|
60
116
|
## License
|
|
61
117
|
|
|
62
118
|
MIT
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BACKEND_URL,
|
|
3
|
+
reach
|
|
4
|
+
} from "./chunk-GU7C5H6L.js";
|
|
5
|
+
|
|
1
6
|
// src/device.ts
|
|
2
7
|
import { execFile } from "child_process";
|
|
3
8
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
4
9
|
import { homedir, platform } from "os";
|
|
5
10
|
import { join } from "path";
|
|
6
|
-
var BACKEND_URL = process.env.PAIGY_BACKEND_URL ?? "https://paigy.ai";
|
|
7
11
|
var AGENT_NAME = process.env.PAIGY_AGENT ?? "mcp-agent";
|
|
8
12
|
var TOKEN_PATH = join(homedir(), ".paigy", "token.json");
|
|
9
13
|
function openBrowser(url) {
|
|
@@ -33,14 +37,14 @@ function deleteToken() {
|
|
|
33
37
|
return true;
|
|
34
38
|
}
|
|
35
39
|
async function revokeToken(token) {
|
|
36
|
-
const res = await
|
|
40
|
+
const res = await reach(`${BACKEND_URL}/api/device/revoke`, {
|
|
37
41
|
method: "POST",
|
|
38
42
|
headers: { authorization: `Bearer ${token}` }
|
|
39
43
|
});
|
|
40
44
|
return res.ok;
|
|
41
45
|
}
|
|
42
46
|
async function requestCode(agent = AGENT_NAME) {
|
|
43
|
-
const res = await
|
|
47
|
+
const res = await reach(`${BACKEND_URL}/api/device/code`, {
|
|
44
48
|
method: "POST",
|
|
45
49
|
headers: { "content-type": "application/json" },
|
|
46
50
|
body: JSON.stringify({ agent })
|
|
@@ -49,7 +53,7 @@ async function requestCode(agent = AGENT_NAME) {
|
|
|
49
53
|
return await res.json();
|
|
50
54
|
}
|
|
51
55
|
async function pollToken(deviceCode) {
|
|
52
|
-
const res = await
|
|
56
|
+
const res = await reach(`${BACKEND_URL}/api/device/token`, {
|
|
53
57
|
method: "POST",
|
|
54
58
|
headers: { "content-type": "application/json" },
|
|
55
59
|
body: JSON.stringify({ device_code: deviceCode })
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BACKEND_URL,
|
|
3
|
+
reach
|
|
4
|
+
} from "./chunk-GU7C5H6L.js";
|
|
5
|
+
|
|
1
6
|
// ../../packages/schema/dist/index.js
|
|
2
7
|
import { z } from "zod";
|
|
3
8
|
var ContextSchema = z.object({
|
|
@@ -134,6 +139,14 @@ var InboxItemSchema = z.object({
|
|
|
134
139
|
createdAt: z.string().datetime(),
|
|
135
140
|
snoozedUntil: z.string().datetime().optional(),
|
|
136
141
|
agentState: AgentStateSchema.default("idle"),
|
|
142
|
+
/** Whose action the item is waiting on: "you" = an agent asked you (the default,
|
|
143
|
+
* every agent→user notification); "agent" = you sent a request and it's awaiting the
|
|
144
|
+
* agent (held in the inbox until the agent replies on the thread). */
|
|
145
|
+
turn: z.enum(["you", "agent"]).default("you"),
|
|
146
|
+
/** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
|
|
147
|
+
* the agent (provider-agnostic; set server-side). Absent = no hard error, though the
|
|
148
|
+
* client may still flag a stall by age. Drives the inbox error badge + Retry. */
|
|
149
|
+
error: z.string().optional(),
|
|
137
150
|
parentId: z.string().optional(),
|
|
138
151
|
select: z.enum(["one", "many", "rank", "confirm"]).default("one"),
|
|
139
152
|
confirmStyle: z.enum(["yesno", "approve"]).default("yesno").describe(
|
|
@@ -151,6 +164,16 @@ var PushTokenSchema = z.object({
|
|
|
151
164
|
alertToken: z.string().min(1).optional(),
|
|
152
165
|
platform: z.literal("ios")
|
|
153
166
|
});
|
|
167
|
+
var MissedCallSchema = z.enum([
|
|
168
|
+
"retry_10m",
|
|
169
|
+
"retry_30m",
|
|
170
|
+
"retry_60m",
|
|
171
|
+
"backoff_gentle",
|
|
172
|
+
"backoff_standard",
|
|
173
|
+
"backoff_aggressive",
|
|
174
|
+
"inbox",
|
|
175
|
+
"dismiss"
|
|
176
|
+
]);
|
|
154
177
|
var UserSettingsSchema = z.object({
|
|
155
178
|
permissions: z.object({
|
|
156
179
|
call: z.boolean(),
|
|
@@ -159,7 +182,10 @@ var UserSettingsSchema = z.object({
|
|
|
159
182
|
}),
|
|
160
183
|
sessionMode: z.enum(["default", "all_calls", "silent"]),
|
|
161
184
|
silentPush: z.boolean(),
|
|
162
|
-
autoCallback: z.boolean()
|
|
185
|
+
autoCallback: z.boolean(),
|
|
186
|
+
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
187
|
+
improveConsent: z.boolean(),
|
|
188
|
+
missedCall: MissedCallSchema.default("backoff_standard")
|
|
163
189
|
});
|
|
164
190
|
var HistoryItemSchema = z.object({
|
|
165
191
|
id: z.string(),
|
|
@@ -181,7 +207,13 @@ var ConnectionSummarySchema = z.object({
|
|
|
181
207
|
agent: z.string(),
|
|
182
208
|
device: z.string().nullable(),
|
|
183
209
|
nickname: z.string(),
|
|
184
|
-
createdAt: z.string().datetime()
|
|
210
|
+
createdAt: z.string().datetime(),
|
|
211
|
+
/** Most recent notification on this connection, either direction. Null = no contact yet.
|
|
212
|
+
* Drives the agents-page recency grouping (Today / This week / …). */
|
|
213
|
+
lastContactAt: z.string().datetime().nullable(),
|
|
214
|
+
/** True = a provider-managed agent running in the provider's cloud (e.g. Anthropic CMA);
|
|
215
|
+
* false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
|
|
216
|
+
managed: z.boolean()
|
|
185
217
|
});
|
|
186
218
|
var CreateRequestSchema = z.object({
|
|
187
219
|
/** The connection (token id) to send to, from GET /api/tokens. */
|
|
@@ -197,15 +229,6 @@ var OAuthStartSchema = z.object({
|
|
|
197
229
|
provider: z.enum(["cma"]),
|
|
198
230
|
returnTo: z.string().min(1)
|
|
199
231
|
});
|
|
200
|
-
var SetProviderSchema = z.object({
|
|
201
|
-
provider: z.enum(["cma"]),
|
|
202
|
-
/** Provider-side agent id (e.g. CMA `agent_…`). */
|
|
203
|
-
agentRef: z.string().min(1),
|
|
204
|
-
/** Provider-side environment id (e.g. CMA `env_…`). */
|
|
205
|
-
environmentId: z.string().min(1),
|
|
206
|
-
/** The delegated API key Paigy holds (in Vault) to spawn sessions on the user's behalf. */
|
|
207
|
-
apiKey: z.string().min(1)
|
|
208
|
-
});
|
|
209
232
|
var DeliveryConfigSchema = z.object({
|
|
210
233
|
tokenId: z.string(),
|
|
211
234
|
mode: DeliveryModeSchema,
|
|
@@ -247,12 +270,16 @@ var DeviceTokenSchema = z.object({
|
|
|
247
270
|
agent: z.string(),
|
|
248
271
|
device: z.string().nullable()
|
|
249
272
|
});
|
|
273
|
+
var SupportRequestSchema = z.object({
|
|
274
|
+
email: z.string().email().max(320),
|
|
275
|
+
message: z.string().trim().min(1).max(5e3),
|
|
276
|
+
name: z.string().trim().max(120).optional()
|
|
277
|
+
});
|
|
250
278
|
|
|
251
279
|
// src/client.ts
|
|
252
280
|
import { existsSync, readFileSync } from "fs";
|
|
253
281
|
import { homedir } from "os";
|
|
254
282
|
import { join } from "path";
|
|
255
|
-
var BACKEND_URL = process.env.PAIGY_BACKEND_URL ?? "https://paigy.ai";
|
|
256
283
|
var TOKEN_PATH = join(homedir(), ".paigy", "token.json");
|
|
257
284
|
function loadToken() {
|
|
258
285
|
if (process.env.PAIGY_TOKEN) return process.env.PAIGY_TOKEN;
|
|
@@ -273,7 +300,7 @@ function ensureAuthed(res) {
|
|
|
273
300
|
}
|
|
274
301
|
async function submitNotification(req) {
|
|
275
302
|
const token = loadToken();
|
|
276
|
-
const res = ensureAuthed(await
|
|
303
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notify`, {
|
|
277
304
|
method: "POST",
|
|
278
305
|
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
279
306
|
body: JSON.stringify(req)
|
|
@@ -290,7 +317,7 @@ async function awaitReply(notificationId, opts = {}) {
|
|
|
290
317
|
const token = loadToken();
|
|
291
318
|
const start = now();
|
|
292
319
|
while (true) {
|
|
293
|
-
const res = ensureAuthed(await
|
|
320
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/await?notificationId=${encodeURIComponent(notificationId)}`, {
|
|
294
321
|
headers: { authorization: `Bearer ${token}` }
|
|
295
322
|
}));
|
|
296
323
|
if (!res.ok) throw new Error(`await failed: ${res.status} ${await res.text()}`);
|
|
@@ -302,14 +329,14 @@ async function awaitReply(notificationId, opts = {}) {
|
|
|
302
329
|
}
|
|
303
330
|
async function checkReplies() {
|
|
304
331
|
const token = loadToken();
|
|
305
|
-
const res = ensureAuthed(await
|
|
332
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/pending`, {
|
|
306
333
|
headers: { authorization: `Bearer ${token}` }
|
|
307
334
|
}));
|
|
308
335
|
if (!res.ok) throw new Error(`check_replies failed: ${res.status} ${await res.text()}`);
|
|
309
336
|
return await res.json();
|
|
310
337
|
}
|
|
311
338
|
async function setTaskState(id, state) {
|
|
312
|
-
const res = ensureAuthed(await
|
|
339
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notify/${id}/state`, {
|
|
313
340
|
method: "PATCH",
|
|
314
341
|
headers: { "content-type": "application/json", authorization: `Bearer ${loadToken()}` },
|
|
315
342
|
body: JSON.stringify({ state })
|
|
@@ -318,7 +345,7 @@ async function setTaskState(id, state) {
|
|
|
318
345
|
return await res.json();
|
|
319
346
|
}
|
|
320
347
|
async function registerDelivery(mode) {
|
|
321
|
-
const res = ensureAuthed(await
|
|
348
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/delivery`, {
|
|
322
349
|
method: "POST",
|
|
323
350
|
headers: { "content-type": "application/json", authorization: `Bearer ${loadToken()}` },
|
|
324
351
|
body: JSON.stringify({ mode })
|
|
@@ -328,7 +355,7 @@ async function registerDelivery(mode) {
|
|
|
328
355
|
}
|
|
329
356
|
async function scheduleCallback(req) {
|
|
330
357
|
const token = loadToken();
|
|
331
|
-
const res = ensureAuthed(await
|
|
358
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/callback`, {
|
|
332
359
|
method: "POST",
|
|
333
360
|
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
334
361
|
body: JSON.stringify(req)
|
|
@@ -338,7 +365,7 @@ async function scheduleCallback(req) {
|
|
|
338
365
|
}
|
|
339
366
|
async function pollAnswer(id) {
|
|
340
367
|
const token = loadToken();
|
|
341
|
-
const res = ensureAuthed(await
|
|
368
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/poll/${id}`, {
|
|
342
369
|
headers: { authorization: `Bearer ${token}` }
|
|
343
370
|
}));
|
|
344
371
|
if (!res.ok) throw new Error(`poll failed: ${res.status} ${await res.text()}`);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/http.ts
|
|
2
|
+
var BACKEND_URL = process.env.PAIGY_BACKEND_URL ?? "https://paigy.ai";
|
|
3
|
+
var NETWORK_MSG = `Can't reach ${BACKEND_URL} \u2014 the connection was blocked or dropped before an HTTP response. If this agent runs in a sandboxed environment with a network allowlist (e.g. Claude Code on the web, CI), ask the user to add the Paigy domain (paigy.ai) to the environment's allowed domains, then retry.`;
|
|
4
|
+
async function reach(url, init) {
|
|
5
|
+
try {
|
|
6
|
+
return await fetch(url, init);
|
|
7
|
+
} catch (e) {
|
|
8
|
+
throw new Error(`${NETWORK_MSG} (${e?.message ?? String(e)})`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
BACKEND_URL,
|
|
14
|
+
reach
|
|
15
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
scheduleCallback,
|
|
12
12
|
setTaskState,
|
|
13
13
|
submitNotification
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-BUELEUTV.js";
|
|
15
15
|
import {
|
|
16
16
|
deleteToken,
|
|
17
17
|
openBrowser,
|
|
@@ -21,7 +21,8 @@ import {
|
|
|
21
21
|
revokeToken,
|
|
22
22
|
saveToken,
|
|
23
23
|
sleep
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-AILC3H3U.js";
|
|
25
|
+
import "./chunk-GU7C5H6L.js";
|
|
25
26
|
|
|
26
27
|
// src/index.ts
|
|
27
28
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -32,7 +33,33 @@ import {
|
|
|
32
33
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
33
34
|
import { execSync } from "child_process";
|
|
34
35
|
import { z } from "zod";
|
|
36
|
+
|
|
37
|
+
// src/schema.ts
|
|
35
38
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
39
|
+
function draft2020(node) {
|
|
40
|
+
if (Array.isArray(node)) return node.map(draft2020);
|
|
41
|
+
if (node && typeof node === "object") {
|
|
42
|
+
const o = node;
|
|
43
|
+
for (const [excl, lim] of [["exclusiveMinimum", "minimum"], ["exclusiveMaximum", "maximum"]]) {
|
|
44
|
+
if (typeof o[excl] === "boolean") {
|
|
45
|
+
if (o[excl] === true && typeof o[lim] === "number") {
|
|
46
|
+
o[excl] = o[lim];
|
|
47
|
+
delete o[lim];
|
|
48
|
+
} else delete o[excl];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (const k of Object.keys(o)) o[k] = draft2020(o[k]);
|
|
52
|
+
return o;
|
|
53
|
+
}
|
|
54
|
+
return node;
|
|
55
|
+
}
|
|
56
|
+
function json(s) {
|
|
57
|
+
const schema = zodToJsonSchema(s, { target: "jsonSchema2019-09", $refStrategy: "none" });
|
|
58
|
+
delete schema.$schema;
|
|
59
|
+
return draft2020(schema);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/index.ts
|
|
36
63
|
var PollAnswerSchema = z.object({
|
|
37
64
|
id: z.string().describe("Request id returned by notify_user.")
|
|
38
65
|
});
|
|
@@ -80,47 +107,47 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
80
107
|
{
|
|
81
108
|
name: "pair",
|
|
82
109
|
description: "Pair this agent with the user's Paigy account (one-time) \u2014 required before notify_user/await_reply work. Two steps: (1) call with NO args to start; it opens the user's browser and returns { verification_uri_complete, user_code, device_code } \u2014 show the user the URL + user_code and ask them to approve. (2) call again passing that device_code to finish; it waits for approval and saves the token. If it returns { status:'pending' }, the user hasn't approved yet \u2014 call again with the same device_code to keep waiting.",
|
|
83
|
-
inputSchema:
|
|
110
|
+
inputSchema: json(PairSchema)
|
|
84
111
|
},
|
|
85
112
|
{
|
|
86
113
|
name: "unpair",
|
|
87
114
|
description: "Log out / unpair this agent from the user's Paigy account: revokes the token server-side (it stops working everywhere) and deletes the local ~/.paigy/token.json. Takes no arguments. After this, notify_user/await_reply won't work until the user pairs again with the pair tool.",
|
|
88
|
-
inputSchema:
|
|
115
|
+
inputSchema: json(z.object({}))
|
|
89
116
|
},
|
|
90
117
|
{
|
|
91
118
|
name: "notify_user",
|
|
92
119
|
description: "Notify the user via Paigy and get a request id to poll for their answer. Provide context.title (a specific, non-empty one-line headline \u2014 this is what the user sees first, and what shows on the ring for a call) and context.description (an array of standalone, non-empty detail chunks the user can selectively ask you to expand). Set `urgency`: 'inbox' (default) drops it silently in their inbox; 'push' is a quiet passive notification (no sound); 'banner' sends a time-sensitive banner/lock-screen push (a 'paige') they tap to open \u2014 for when you need them soon-ish but not enough to ring them; 'call' rings their phone now as a voice call \u2014 only when you genuinely need them in the moment (blocked/waiting, time-sensitive). ON A CALL, your title + description are READ ALOUD by a voice \u2014 write them to be HEARD, not read: keep it short and conversational, front-load the ask, and refer to things BY NAME, not by ID or code (say 'the pull request about the agents page', not 'PR #235'; 'the login-bug ticket', not 'ABC-1234'). Spell out only what's natural to say out loud. MATCH the answer shape to the question \u2014 pick the best tool for the job, not always yes/no. The user can ALWAYS add free text on top of any shape, so structuring loses nothing. Choose `select`: yes/no \u2192 select:'confirm' \u2192 {kind:'confirm', approved:boolean}. Approve/deny an action \u2192 select:'confirm' + confirmStyle:'approve' \u2192 {kind:'confirm', approved:boolean}. Both are answerable right from the banner. Pick one of several \u2192 options + select:'one' \u2192 {kind:'option', optionId}. Pick several / a subset \u2192 options + select:'many' \u2192 {kind:'multi', optionIds:[...]}. Rank or prioritize \u2192 options + select:'rank', user taps in preferred order \u2192 {kind:'ranked', optionIds:[...]}. For visual choices give each option a sandboxed `html` or an `image` preview (e.g. layout/UI alternatives). Only leave options off (pure free text / voice) when the answer genuinely can't be structured. Plus optional visuals. If a reply comes back as {kind:'clarify', chunks:[...]}, the user wants more detail on those chunks \u2014 respond via notify_user with the SAME threadId and an expanded description. Pass `threadId` from a prior notify_user result or an await_reply reply to continue that conversation thread; omit it to start a new one. To follow up on a call (e.g. the user asked you to 'call me back when it's done'), reuse the threadId from that call's reply so it threads as the same conversation.",
|
|
93
|
-
inputSchema:
|
|
120
|
+
inputSchema: json(NotifyRequestSchema)
|
|
94
121
|
},
|
|
95
122
|
{
|
|
96
123
|
name: "poll_answer",
|
|
97
124
|
description: "Fetch the user's answer to a previous notify_user request. Returns status pending | answered | ignored, with the answer once present. Errors if the id is unknown or expired.",
|
|
98
|
-
inputSchema:
|
|
125
|
+
inputSchema: json(PollAnswerSchema)
|
|
99
126
|
},
|
|
100
127
|
{
|
|
101
128
|
name: "await_reply",
|
|
102
129
|
description: "Wait for the user's reply to THE specific notification you sent for the current request (pass its id from notify_user). This is how you wait for your answer in-context. Polls ~5 min; returns { type:'reply', answer } when they respond, { type:'remind', remindInSeconds } on snooze (ScheduleWakeup then await_reply again), or { type:'idle' }. Scoped to that one notification \u2014 it NEVER returns replies meant for other notifications/requests, so concurrent requests don't cross. A CALL answer can come back as {kind:'turns', turns:[{prompt,reply}]} \u2014 the ordered log of that call. Read turns[0].reply as the user's main instruction and any later turns as their follow-up (e.g. the end-of-call 'call me back when it's done / I have a blocking question' reply). If they asked for a callback, re-engage in the SAME thread (notify_user with the reply's threadId) when the task is done or you hit a blocker \u2014 urgency:'call' for a blocker, 'banner'/'push'/'inbox' for done. Paigy has no scheduler; the callback is yours to send (use ScheduleWakeup/cron for timing).",
|
|
103
|
-
inputSchema:
|
|
130
|
+
inputSchema: json(AwaitReplySchema)
|
|
104
131
|
},
|
|
105
132
|
{
|
|
106
133
|
name: "check_replies",
|
|
107
134
|
description: "The catch-up router for everything NOT tied to your current request: returns replies the user sent that you haven't seen yet (now marked seen), still-pending notifications you sent, and `requests` \u2014 new requests the user started toward you (each { notificationId, threadId, text }, returned once). Act on each request and reply with notify_user on the SAME threadId. Use it when booting up / starting a session, or when you've been waiting a long time on something else, to discover acks or work you're unaware of. To wait on a request you just sent, use await_reply instead. Also returns owedCallbacks: callbacks now due that you promised \u2014 fulfill each with notify_user on its threadId.",
|
|
108
|
-
inputSchema:
|
|
135
|
+
inputSchema: json(CheckRepliesSchema)
|
|
109
136
|
},
|
|
110
137
|
{
|
|
111
138
|
name: "set_task_state",
|
|
112
139
|
description: "Report progress on a request you received: in_progress (you started working), completed (done), or needs_input (you need more from the user \u2014 usually paired with a notify_user carrying parentId = this request's id).",
|
|
113
|
-
inputSchema:
|
|
140
|
+
inputSchema: json(SetTaskStateToolSchema)
|
|
114
141
|
},
|
|
115
142
|
{
|
|
116
143
|
name: "register_delivery",
|
|
117
144
|
description: "Choose how Paigy reaches you when a reply or new request lands. 'self_hosted' makes Paigy PUSH it instantly over a realtime channel instead of you polling \u2014 but it only helps if a `paigy listen` daemon is running (npx -y @paigy/mcp paigy-listen), which the user runs once. 'poll' (default) keeps the catch-up model (check_replies / await_reply). Either way no work is ever lost: a missed push is reconciled by your next check_replies sweep.",
|
|
118
|
-
inputSchema:
|
|
145
|
+
inputSchema: json(RegisterDeliveryToolSchema)
|
|
119
146
|
},
|
|
120
147
|
{
|
|
121
148
|
name: "schedule_callback",
|
|
122
149
|
description: "Promise the user a follow-up you'll keep even if you go idle. Use it when they ask you to report back: trigger 'on_done' (when you finish \u2014 fires when you call set_task_state completed), 'on_blocked' (if you hit a blocker \u2014 fires on set_task_state needs_input), or 'scheduled' with dueInSeconds (e.g. 'remind me in 10 min'). Pass the threadId of the conversation and a short note. Fulfill it by calling notify_user on that threadId; check_replies re-lists due callbacks until you do.",
|
|
123
|
-
inputSchema:
|
|
150
|
+
inputSchema: json(ScheduleCallbackSchema)
|
|
124
151
|
}
|
|
125
152
|
]
|
|
126
153
|
}));
|
package/dist/listen.js
CHANGED
package/dist/onboard.js
CHANGED