@paigy/mcp 0.8.1 → 0.8.4
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
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,7 +39,7 @@ 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
|
|
@@ -50,7 +50,7 @@ It talks to the hosted backend by default — no config needed. Set
|
|
|
50
50
|
Codex speaks MCP, so the same server drops in. Add it (writes `~/.codex/config.toml`):
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
|
-
codex mcp add paigy --env PAIGY_AGENT=codex -- npx -y @paigy/mcp
|
|
53
|
+
codex mcp add paigy --env PAIGY_AGENT=codex -- npx -y @paigy/mcp@latest
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
Or add the entry to `~/.codex/config.toml` by hand:
|
|
@@ -58,16 +58,16 @@ Or add the entry to `~/.codex/config.toml` by hand:
|
|
|
58
58
|
```toml
|
|
59
59
|
[mcp_servers.paigy]
|
|
60
60
|
command = "npx"
|
|
61
|
-
args = ["-y", "@paigy/mcp"]
|
|
61
|
+
args = ["-y", "@paigy/mcp@latest"]
|
|
62
62
|
env = { PAIGY_AGENT = "codex" }
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
Then pair: `npx -p @paigy/mcp paigy-mcp-onboard` (or have the agent call the `pair` tool).
|
|
65
|
+
Then pair: `npx -p @paigy/mcp@latest paigy-mcp-onboard` (or have the agent call the `pair` tool).
|
|
66
66
|
|
|
67
67
|
### Gemini CLI
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
|
-
gemini mcp add -s user -e PAIGY_AGENT=gemini paigy npx -y @paigy/mcp
|
|
70
|
+
gemini mcp add -s user -e PAIGY_AGENT=gemini paigy npx -y @paigy/mcp@latest
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
`-s user` makes it available across all projects — omit it for just the current one.
|
|
@@ -78,14 +78,14 @@ Or add the entry to `~/.gemini/settings.json`:
|
|
|
78
78
|
"mcpServers": {
|
|
79
79
|
"paigy": {
|
|
80
80
|
"command": "npx",
|
|
81
|
-
"args": ["-y", "@paigy/mcp"],
|
|
81
|
+
"args": ["-y", "@paigy/mcp@latest"],
|
|
82
82
|
"env": { "PAIGY_AGENT": "gemini" }
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
Then pair: `npx -p @paigy/mcp paigy-mcp-onboard`.
|
|
88
|
+
Then pair: `npx -p @paigy/mcp@latest paigy-mcp-onboard`.
|
|
89
89
|
|
|
90
90
|
> `PAIGY_AGENT` just names the agent on the device-approval screen (defaults to
|
|
91
91
|
> `mcp-agent`) — set it per client so you can tell your connected agents apart.
|
|
@@ -102,6 +102,17 @@ Then pair: `npx -p @paigy/mcp paigy-mcp-onboard`.
|
|
|
102
102
|
|
|
103
103
|
- `PAIGY_BACKEND_URL` — the Paigy API base (defaults to the hosted backend).
|
|
104
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
|
+
|
|
105
116
|
## License
|
|
106
117
|
|
|
107
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 })
|
|
@@ -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
|
+
};
|
|
@@ -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({
|
|
@@ -157,7 +162,8 @@ var SnoozeRequestSchema = z.object({
|
|
|
157
162
|
var PushTokenSchema = z.object({
|
|
158
163
|
voipToken: z.string().min(1).optional(),
|
|
159
164
|
alertToken: z.string().min(1).optional(),
|
|
160
|
-
|
|
165
|
+
fcmToken: z.string().min(1).optional(),
|
|
166
|
+
platform: z.enum(["ios", "android"])
|
|
161
167
|
});
|
|
162
168
|
var MissedCallSchema = z.enum([
|
|
163
169
|
"retry_10m",
|
|
@@ -275,7 +281,6 @@ var SupportRequestSchema = z.object({
|
|
|
275
281
|
import { existsSync, readFileSync } from "fs";
|
|
276
282
|
import { homedir } from "os";
|
|
277
283
|
import { join } from "path";
|
|
278
|
-
var BACKEND_URL = process.env.PAIGY_BACKEND_URL ?? "https://paigy.ai";
|
|
279
284
|
var TOKEN_PATH = join(homedir(), ".paigy", "token.json");
|
|
280
285
|
function loadToken() {
|
|
281
286
|
if (process.env.PAIGY_TOKEN) return process.env.PAIGY_TOKEN;
|
|
@@ -296,7 +301,7 @@ function ensureAuthed(res) {
|
|
|
296
301
|
}
|
|
297
302
|
async function submitNotification(req) {
|
|
298
303
|
const token = loadToken();
|
|
299
|
-
const res = ensureAuthed(await
|
|
304
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notify`, {
|
|
300
305
|
method: "POST",
|
|
301
306
|
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
302
307
|
body: JSON.stringify(req)
|
|
@@ -313,7 +318,7 @@ async function awaitReply(notificationId, opts = {}) {
|
|
|
313
318
|
const token = loadToken();
|
|
314
319
|
const start = now();
|
|
315
320
|
while (true) {
|
|
316
|
-
const res = ensureAuthed(await
|
|
321
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/await?notificationId=${encodeURIComponent(notificationId)}`, {
|
|
317
322
|
headers: { authorization: `Bearer ${token}` }
|
|
318
323
|
}));
|
|
319
324
|
if (!res.ok) throw new Error(`await failed: ${res.status} ${await res.text()}`);
|
|
@@ -325,14 +330,14 @@ async function awaitReply(notificationId, opts = {}) {
|
|
|
325
330
|
}
|
|
326
331
|
async function checkReplies() {
|
|
327
332
|
const token = loadToken();
|
|
328
|
-
const res = ensureAuthed(await
|
|
333
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/pending`, {
|
|
329
334
|
headers: { authorization: `Bearer ${token}` }
|
|
330
335
|
}));
|
|
331
336
|
if (!res.ok) throw new Error(`check_replies failed: ${res.status} ${await res.text()}`);
|
|
332
337
|
return await res.json();
|
|
333
338
|
}
|
|
334
339
|
async function setTaskState(id, state) {
|
|
335
|
-
const res = ensureAuthed(await
|
|
340
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notify/${id}/state`, {
|
|
336
341
|
method: "PATCH",
|
|
337
342
|
headers: { "content-type": "application/json", authorization: `Bearer ${loadToken()}` },
|
|
338
343
|
body: JSON.stringify({ state })
|
|
@@ -341,7 +346,7 @@ async function setTaskState(id, state) {
|
|
|
341
346
|
return await res.json();
|
|
342
347
|
}
|
|
343
348
|
async function registerDelivery(mode) {
|
|
344
|
-
const res = ensureAuthed(await
|
|
349
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/delivery`, {
|
|
345
350
|
method: "POST",
|
|
346
351
|
headers: { "content-type": "application/json", authorization: `Bearer ${loadToken()}` },
|
|
347
352
|
body: JSON.stringify({ mode })
|
|
@@ -351,7 +356,7 @@ async function registerDelivery(mode) {
|
|
|
351
356
|
}
|
|
352
357
|
async function scheduleCallback(req) {
|
|
353
358
|
const token = loadToken();
|
|
354
|
-
const res = ensureAuthed(await
|
|
359
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/callback`, {
|
|
355
360
|
method: "POST",
|
|
356
361
|
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
357
362
|
body: JSON.stringify(req)
|
|
@@ -361,7 +366,7 @@ async function scheduleCallback(req) {
|
|
|
361
366
|
}
|
|
362
367
|
async function pollAnswer(id) {
|
|
363
368
|
const token = loadToken();
|
|
364
|
-
const res = ensureAuthed(await
|
|
369
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/poll/${id}`, {
|
|
365
370
|
headers: { authorization: `Bearer ${token}` }
|
|
366
371
|
}));
|
|
367
372
|
if (!res.ok) throw new Error(`poll failed: ${res.status} ${await res.text()}`);
|
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-SLK7NHHQ.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";
|
|
@@ -98,7 +99,7 @@ var server = new Server(
|
|
|
98
99
|
{ name: "paigy", version: "0.0.0" },
|
|
99
100
|
{
|
|
100
101
|
capabilities: { tools: {} },
|
|
101
|
-
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. When you need a decision or input, MATCH the answer shape to the question \u2014 don't default everything to free text, and don't reflexively make everything yes/no. Pick the best tool for the job: yes/no \u2192 select:'confirm'; approve/deny an action \u2192 select:'confirm' + confirmStyle:'approve'; pick one of several \u2192 options + select:'one'; pick several / a subset \u2192 options + select:'many'; rank or prioritize \u2192 options + select:'rank'. Reserve open-ended free text only for answers that genuinely can't be structured (the user can always add free text on top of any shape). If a reply comes back as { kind: 'clarify', chunks: [...] }, the user wants more detail on those chunks \u2014 respond by calling notify_user again with the SAME threadId and an expanded description covering them. When urgency is 'call', remember the title + description are spoken aloud \u2014 write them short and conversational, and name things instead of using IDs (e.g. 'the pull request about the agents page', not 'PR #235'). When the user asks you to follow up later \u2014 when you're done, if you're blocked, or at a set time \u2014 record it with schedule_callback so you don't drop it if you go idle."
|
|
102
|
+
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. When you need a decision or input, MATCH the answer shape to the question \u2014 don't default everything to free text, and don't reflexively make everything yes/no. Pick the best tool for the job: yes/no \u2192 select:'confirm'; approve/deny an action \u2192 select:'confirm' + confirmStyle:'approve'; pick one of several \u2192 options + select:'one'; pick several / a subset \u2192 options + select:'many'; rank or prioritize \u2192 options + select:'rank'. Reserve open-ended free text only for answers that genuinely can't be structured (the user can always add free text on top of any shape). If a reply comes back as { kind: 'clarify', chunks: [...] }, the user wants more detail on those chunks \u2014 respond by calling notify_user again with the SAME threadId and an expanded description covering them. When urgency is 'call', remember the title + description are spoken aloud \u2014 write them short and conversational, and name things instead of using IDs (e.g. 'the pull request about the agents page', not 'PR #235'). When the user asks you to follow up later \u2014 when you're done, if you're blocked, or at a set time \u2014 record it with schedule_callback so you don't drop it if you go idle. If you're about to start a genuinely long-running or blocking piece of work \u2014 one where the user would otherwise sit and wait \u2014 mention ONCE, in passing, that you can text or call them when it's done or if you hit a blocker, instead of them needing to babysit the terminal. Don't offer this for quick tasks, and don't repeat the offer if they've already said yes or no earlier in the conversation. Escalate silence, don't just wait on it: if you notified at a lower urgency (inbox/push/banner) for something that's genuinely blocking real progress, call await_reply up to twice (~5 min each, ~10 min total) \u2014 if it's still idle after that AND the item is genuinely blocking, send a fresh notify_user on the SAME threadId at urgency:'call'. Skip this for anything that isn't truly blocking; a normal question can just sit in the inbox."
|
|
102
103
|
}
|
|
103
104
|
);
|
|
104
105
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
package/dist/listen.js
CHANGED
package/dist/onboard.js
CHANGED