@pergy-ai/mcp 0.1.0 → 0.2.1
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 +10 -7
- package/dist/index.js +61 -13
- package/dist/onboard.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,21 +4,20 @@ A voice inbox for your AI agents. This MCP server lets an agent **notify a user*
|
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
|
-
No clone needed
|
|
7
|
+
No clone needed. Add it to Claude Code (`-s user` = available in every project; drop it for just the current one):
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npx @pergy-ai/mcp
|
|
10
|
+
claude mcp add pergy -s user -- npx -y @pergy-ai/mcp
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Or wire it into
|
|
13
|
+
Or wire it into any MCP client config:
|
|
14
14
|
|
|
15
15
|
```json
|
|
16
16
|
{
|
|
17
17
|
"mcpServers": {
|
|
18
18
|
"pergy": {
|
|
19
19
|
"command": "npx",
|
|
20
|
-
"args": ["-y", "@pergy-ai/mcp"]
|
|
21
|
-
"env": { "PERGY_BACKEND_URL": "https://pergy.ai" }
|
|
20
|
+
"args": ["-y", "@pergy-ai/mcp"]
|
|
22
21
|
}
|
|
23
22
|
}
|
|
24
23
|
}
|
|
@@ -30,10 +29,14 @@ First-time pairing (link the server to your Pergy account):
|
|
|
30
29
|
npx -p @pergy-ai/mcp pergy-mcp-onboard
|
|
31
30
|
```
|
|
32
31
|
|
|
32
|
+
It talks to the hosted backend by default — no config needed. Set
|
|
33
|
+
`PERGY_BACKEND_URL=http://localhost:3000` only for local development.
|
|
34
|
+
|
|
33
35
|
## Tools
|
|
34
36
|
|
|
35
|
-
- **`notify_user`** — notify the user (three standalone context tiers: short / medium / long, plus optional options
|
|
36
|
-
- **`
|
|
37
|
+
- **`notify_user`** — notify the user (three standalone context tiers: short / medium / long, 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.
|
|
38
|
+
- **`await_reply`** — wait for the user's reply to a specific notification (pass its id). Scoped: will not return replies meant for other notifications. Returns `reply` / `remind` / `idle`.
|
|
39
|
+
- **`check_replies`** — catch-up sweep: returns replies you haven't consumed yet (now marked seen) plus still-pending notifications.
|
|
37
40
|
- **`poll_answer`** — fetch the answer to a specific request by id.
|
|
38
41
|
- **`set_task_state`** — report progress on a request: `in_progress` / `completed` / `needs_input`.
|
|
39
42
|
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,15 @@ var ContextTiersSchema = z.object({
|
|
|
18
18
|
});
|
|
19
19
|
var OptionSchema = z.object({
|
|
20
20
|
id: z.string(),
|
|
21
|
-
label: z.string()
|
|
21
|
+
label: z.string(),
|
|
22
|
+
// .describe() flows into the MCP notify_user JSON schema (zodToJsonSchema), so
|
|
23
|
+
// the constraints below are what an agent reads when deciding to use these.
|
|
24
|
+
html: z.string().max(16384).describe(
|
|
25
|
+
"Optional sandboxed HTML/CSS preview for a visual 'pick one' (shown in the option card). Untrusted-sandboxed: NO JavaScript, NO external network or images \u2014 inline CSS and data: URIs only; <=16KB. Use for layout/CSS mockups, tables, diffs. For a hosted image use `image` instead."
|
|
26
|
+
).optional(),
|
|
27
|
+
image: z.string().url().describe(
|
|
28
|
+
"Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
|
|
29
|
+
).optional()
|
|
22
30
|
});
|
|
23
31
|
var VisualSchema = z.object({
|
|
24
32
|
url: z.string().url(),
|
|
@@ -71,6 +79,14 @@ var AwaitItemSchema = z.discriminatedUnion("type", [
|
|
|
71
79
|
}),
|
|
72
80
|
z.object({ type: z.literal("idle") })
|
|
73
81
|
]);
|
|
82
|
+
var PendingRepliesSchema = z.object({
|
|
83
|
+
replies: z.array(
|
|
84
|
+
z.object({ threadId: z.string(), notificationId: z.string(), answer: UserAnswerSchema })
|
|
85
|
+
),
|
|
86
|
+
pending: z.array(
|
|
87
|
+
z.object({ threadId: z.string(), notificationId: z.string(), createdAt: z.string() })
|
|
88
|
+
)
|
|
89
|
+
});
|
|
74
90
|
var NotifyResponseSchema = z.object({
|
|
75
91
|
id: z.string(),
|
|
76
92
|
status: NotifyStatusSchema,
|
|
@@ -85,6 +101,7 @@ var UserResponseSchema = z.object({
|
|
|
85
101
|
});
|
|
86
102
|
var InboxItemSchema = z.object({
|
|
87
103
|
id: z.string(),
|
|
104
|
+
status: NotifyStatusSchema,
|
|
88
105
|
context: ContextTiersSchema,
|
|
89
106
|
options: z.array(OptionSchema).optional(),
|
|
90
107
|
visuals: z.array(VisualSchema).optional(),
|
|
@@ -101,6 +118,13 @@ var SnoozeRequestSchema = z.object({
|
|
|
101
118
|
requestId: z.string(),
|
|
102
119
|
until: z.string().datetime()
|
|
103
120
|
});
|
|
121
|
+
var PushTokenSchema = z.object({
|
|
122
|
+
voipToken: z.string().min(1),
|
|
123
|
+
platform: z.enum(["ios"])
|
|
124
|
+
});
|
|
125
|
+
var UserSettingsSchema = z.object({
|
|
126
|
+
callsEnabled: z.boolean()
|
|
127
|
+
});
|
|
104
128
|
var DeviceAgentTokenSchema = z.object({
|
|
105
129
|
token: z.string(),
|
|
106
130
|
deviceId: z.string(),
|
|
@@ -139,7 +163,7 @@ import { zodToJsonSchema } from "zod-to-json-schema";
|
|
|
139
163
|
import { existsSync, readFileSync } from "fs";
|
|
140
164
|
import { homedir } from "os";
|
|
141
165
|
import { join } from "path";
|
|
142
|
-
var BACKEND_URL = process.env.PERGY_BACKEND_URL ?? "
|
|
166
|
+
var BACKEND_URL = process.env.PERGY_BACKEND_URL ?? "https://pergy.ai";
|
|
143
167
|
var TOKEN_PATH = join(homedir(), ".pergy", "token.json");
|
|
144
168
|
function loadToken() {
|
|
145
169
|
if (process.env.PERGY_TOKEN) return process.env.PERGY_TOKEN;
|
|
@@ -164,7 +188,7 @@ async function submitNotification(req) {
|
|
|
164
188
|
return await res.json();
|
|
165
189
|
}
|
|
166
190
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
167
|
-
async function
|
|
191
|
+
async function awaitReply(notificationId, opts = {}) {
|
|
168
192
|
const intervalMs = opts.intervalMs ?? 5e3;
|
|
169
193
|
const windowMs = opts.windowMs ?? 3e5;
|
|
170
194
|
const doSleep = opts.sleep ?? sleep;
|
|
@@ -172,7 +196,7 @@ async function awaitTask(opts = {}) {
|
|
|
172
196
|
const token = loadToken();
|
|
173
197
|
const start = now();
|
|
174
198
|
while (true) {
|
|
175
|
-
const res = await fetch(`${BACKEND_URL}/api/await`, {
|
|
199
|
+
const res = await fetch(`${BACKEND_URL}/api/await?notificationId=${encodeURIComponent(notificationId)}`, {
|
|
176
200
|
headers: { authorization: `Bearer ${token}` }
|
|
177
201
|
});
|
|
178
202
|
if (!res.ok) throw new Error(`await failed: ${res.status} ${await res.text()}`);
|
|
@@ -182,6 +206,14 @@ async function awaitTask(opts = {}) {
|
|
|
182
206
|
await doSleep(intervalMs);
|
|
183
207
|
}
|
|
184
208
|
}
|
|
209
|
+
async function checkReplies() {
|
|
210
|
+
const token = loadToken();
|
|
211
|
+
const res = await fetch(`${BACKEND_URL}/api/pending`, {
|
|
212
|
+
headers: { authorization: `Bearer ${token}` }
|
|
213
|
+
});
|
|
214
|
+
if (!res.ok) throw new Error(`check_replies failed: ${res.status} ${await res.text()}`);
|
|
215
|
+
return await res.json();
|
|
216
|
+
}
|
|
185
217
|
async function setTaskState(id, state) {
|
|
186
218
|
const res = await fetch(`${BACKEND_URL}/api/notify/${id}/state`, {
|
|
187
219
|
method: "PATCH",
|
|
@@ -204,7 +236,10 @@ async function pollAnswer(id) {
|
|
|
204
236
|
var PollAnswerSchema = z2.object({
|
|
205
237
|
id: z2.string().describe("Request id returned by notify_user.")
|
|
206
238
|
});
|
|
207
|
-
var
|
|
239
|
+
var AwaitReplySchema = z2.object({
|
|
240
|
+
notificationId: z2.string().describe("The id returned by notify_user \u2014 waits for the user's reply to THIS notification only.")
|
|
241
|
+
});
|
|
242
|
+
var CheckRepliesSchema = z2.object({});
|
|
208
243
|
var SetTaskStateToolSchema = z2.object({
|
|
209
244
|
notificationId: z2.string(),
|
|
210
245
|
state: SetTaskStateSchema.shape.state
|
|
@@ -227,13 +262,16 @@ function detectGit() {
|
|
|
227
262
|
}
|
|
228
263
|
var server = new Server(
|
|
229
264
|
{ name: "pergy", version: "0.0.0" },
|
|
230
|
-
{
|
|
265
|
+
{
|
|
266
|
+
capabilities: { tools: {} },
|
|
267
|
+
instructions: "On startup, call check_replies once to pick up any replies or pending work you missed while away. To wait for the answer to something you just asked, call await_reply with that notification's id \u2014 it's scoped to that one request, so it never returns replies meant for other requests. Use check_replies again only when re-booting or after waiting a long time on something else. Never end a turn that still needs the user without notify_user + await_reply."
|
|
268
|
+
}
|
|
231
269
|
);
|
|
232
270
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
233
271
|
tools: [
|
|
234
272
|
{
|
|
235
273
|
name: "notify_user",
|
|
236
|
-
description: "Notify the user via Pergy and get a request id to poll for their answer. Provide context in three STANDALONE tiers (each must communicate on its own, not as a delta): short = minimum words, a headline; medium = ~3 sentences; long = 1-3 paragraphs. Plus optional options and visuals. Pass `threadId` from a prior notify_user result or an
|
|
274
|
+
description: "Notify the user via Pergy and get a request id to poll for their answer. Provide context in three STANDALONE tiers (each must communicate on its own, not as a delta): short = minimum words, a headline; medium = ~3 sentences; long = 1-3 paragraphs. Plus optional options (answerable choices; each may carry a sandboxed `html` or an `image` preview for a visual 'pick one' \u2014 e.g. show layout/UI alternatives) and visuals. 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.",
|
|
237
275
|
inputSchema: zodToJsonSchema(NotifyRequestSchema, { target: "openApi3" })
|
|
238
276
|
},
|
|
239
277
|
{
|
|
@@ -242,9 +280,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
242
280
|
inputSchema: zodToJsonSchema(PollAnswerSchema, { target: "openApi3" })
|
|
243
281
|
},
|
|
244
282
|
{
|
|
245
|
-
name: "
|
|
246
|
-
description: "Wait for the user's
|
|
247
|
-
inputSchema: zodToJsonSchema(
|
|
283
|
+
name: "await_reply",
|
|
284
|
+
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.",
|
|
285
|
+
inputSchema: zodToJsonSchema(AwaitReplySchema, { target: "openApi3" })
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
name: "check_replies",
|
|
289
|
+
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) plus still-pending notifications you sent. 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.",
|
|
290
|
+
inputSchema: zodToJsonSchema(CheckRepliesSchema, { target: "openApi3" })
|
|
248
291
|
},
|
|
249
292
|
{
|
|
250
293
|
name: "set_task_state",
|
|
@@ -271,11 +314,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
271
314
|
const result = await pollAnswer(id);
|
|
272
315
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
273
316
|
}
|
|
274
|
-
case "
|
|
275
|
-
|
|
276
|
-
const item = await
|
|
317
|
+
case "await_reply": {
|
|
318
|
+
const { notificationId } = AwaitReplySchema.parse(request.params.arguments);
|
|
319
|
+
const item = await awaitReply(notificationId);
|
|
277
320
|
return { content: [{ type: "text", text: JSON.stringify(item) }] };
|
|
278
321
|
}
|
|
322
|
+
case "check_replies": {
|
|
323
|
+
CheckRepliesSchema.parse(request.params.arguments ?? {});
|
|
324
|
+
const result = await checkReplies();
|
|
325
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
326
|
+
}
|
|
279
327
|
case "set_task_state": {
|
|
280
328
|
const { notificationId, state } = SetTaskStateToolSchema.parse(request.params.arguments);
|
|
281
329
|
const result = await setTaskState(notificationId, state);
|
package/dist/onboard.js
CHANGED
|
@@ -5,7 +5,7 @@ import { execFile } from "child_process";
|
|
|
5
5
|
import { mkdirSync, writeFileSync } from "fs";
|
|
6
6
|
import { homedir, platform } from "os";
|
|
7
7
|
import { join } from "path";
|
|
8
|
-
var BACKEND_URL = process.env.PERGY_BACKEND_URL ?? "
|
|
8
|
+
var BACKEND_URL = process.env.PERGY_BACKEND_URL ?? "https://pergy.ai";
|
|
9
9
|
var AGENT_NAME = process.env.PERGY_AGENT ?? "mcp-agent";
|
|
10
10
|
var TOKEN_PATH = join(homedir(), ".pergy", "token.json");
|
|
11
11
|
function openBrowser(url) {
|