@paigy/mcp 0.4.0 → 0.5.0
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 +1 -1
- package/dist/chunk-BSUXW66R.js +52 -0
- package/dist/index.js +63 -3
- package/dist/onboard.js +9 -47
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/device.ts
|
|
2
|
+
import { execFile } from "child_process";
|
|
3
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
4
|
+
import { homedir, platform } from "os";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
var BACKEND_URL = process.env.PAIGY_BACKEND_URL ?? "https://paigy.ai";
|
|
7
|
+
var AGENT_NAME = process.env.PAIGY_AGENT ?? "mcp-agent";
|
|
8
|
+
var TOKEN_PATH = join(homedir(), ".paigy", "token.json");
|
|
9
|
+
function openBrowser(url) {
|
|
10
|
+
const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open";
|
|
11
|
+
const args = platform() === "win32" ? ["/c", "start", url] : [url];
|
|
12
|
+
execFile(cmd, args, () => {
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function saveToken(token) {
|
|
16
|
+
mkdirSync(join(homedir(), ".paigy"), { recursive: true });
|
|
17
|
+
writeFileSync(TOKEN_PATH, JSON.stringify(token, null, 2) + "\n", { mode: 384 });
|
|
18
|
+
}
|
|
19
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
20
|
+
async function requestCode(agent = AGENT_NAME) {
|
|
21
|
+
const res = await fetch(`${BACKEND_URL}/api/device/code`, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: { "content-type": "application/json" },
|
|
24
|
+
body: JSON.stringify({ agent })
|
|
25
|
+
});
|
|
26
|
+
if (!res.ok) throw new Error(`/api/device/code failed: ${res.status} ${await res.text()}`);
|
|
27
|
+
return await res.json();
|
|
28
|
+
}
|
|
29
|
+
async function pollToken(deviceCode) {
|
|
30
|
+
const res = await fetch(`${BACKEND_URL}/api/device/token`, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: { "content-type": "application/json" },
|
|
33
|
+
body: JSON.stringify({ device_code: deviceCode })
|
|
34
|
+
});
|
|
35
|
+
if (res.ok) return await res.json();
|
|
36
|
+
const body = await res.json().catch(() => ({ error: "unknown" }));
|
|
37
|
+
const err = body.error ?? "unknown";
|
|
38
|
+
if (err === "authorization_pending") return null;
|
|
39
|
+
if (err === "expired_token") throw new Error("Device code expired. Start pairing again.");
|
|
40
|
+
if (err === "access_denied") throw new Error("Access denied by user. Pairing cancelled.");
|
|
41
|
+
throw new Error(`Unexpected error from /api/device/token: ${err}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
AGENT_NAME,
|
|
46
|
+
TOKEN_PATH,
|
|
47
|
+
openBrowser,
|
|
48
|
+
saveToken,
|
|
49
|
+
sleep,
|
|
50
|
+
requestCode,
|
|
51
|
+
pollToken
|
|
52
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
openBrowser,
|
|
4
|
+
pollToken,
|
|
5
|
+
requestCode,
|
|
6
|
+
saveToken,
|
|
7
|
+
sleep
|
|
8
|
+
} from "./chunk-BSUXW66R.js";
|
|
2
9
|
|
|
3
10
|
// src/index.ts
|
|
4
11
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -183,7 +190,7 @@ function loadToken() {
|
|
|
183
190
|
}
|
|
184
191
|
return "";
|
|
185
192
|
}
|
|
186
|
-
var ONBOARD_MSG = "Not paired with Paigy yet \u2014
|
|
193
|
+
var ONBOARD_MSG = "Not paired with Paigy yet \u2014 call the `pair` tool to connect this agent (it returns an approval link to show the user), then retry. Manual fallback: `npx -y @paigy/mcp paigy-mcp-onboard`.";
|
|
187
194
|
function ensureAuthed(res) {
|
|
188
195
|
if (res.status === 401) throw new Error(ONBOARD_MSG);
|
|
189
196
|
return res;
|
|
@@ -198,11 +205,11 @@ async function submitNotification(req) {
|
|
|
198
205
|
if (!res.ok) throw new Error(`notify failed: ${res.status} ${await res.text()}`);
|
|
199
206
|
return await res.json();
|
|
200
207
|
}
|
|
201
|
-
var
|
|
208
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
202
209
|
async function awaitReply(notificationId, opts = {}) {
|
|
203
210
|
const intervalMs = opts.intervalMs ?? 5e3;
|
|
204
211
|
const windowMs = opts.windowMs ?? 3e5;
|
|
205
|
-
const doSleep = opts.sleep ??
|
|
212
|
+
const doSleep = opts.sleep ?? sleep2;
|
|
206
213
|
const now = opts.now ?? Date.now;
|
|
207
214
|
const token = loadToken();
|
|
208
215
|
const start = now();
|
|
@@ -251,6 +258,9 @@ var AwaitReplySchema = z2.object({
|
|
|
251
258
|
notificationId: z2.string().describe("The id returned by notify_user \u2014 waits for the user's reply to THIS notification only.")
|
|
252
259
|
});
|
|
253
260
|
var CheckRepliesSchema = z2.object({});
|
|
261
|
+
var PairSchema = z2.object({
|
|
262
|
+
device_code: z2.string().optional().describe("Omit to start pairing (returns an approval link to show the user). Pass the device_code from that first call to finish, once the user has approved.")
|
|
263
|
+
});
|
|
254
264
|
var SetTaskStateToolSchema = z2.object({
|
|
255
265
|
notificationId: z2.string(),
|
|
256
266
|
state: SetTaskStateSchema.shape.state
|
|
@@ -280,6 +290,11 @@ var server = new Server(
|
|
|
280
290
|
);
|
|
281
291
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
282
292
|
tools: [
|
|
293
|
+
{
|
|
294
|
+
name: "pair",
|
|
295
|
+
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.",
|
|
296
|
+
inputSchema: zodToJsonSchema(PairSchema, { target: "openApi3" })
|
|
297
|
+
},
|
|
283
298
|
{
|
|
284
299
|
name: "notify_user",
|
|
285
300
|
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; 'call' rings their phone now as a voice call \u2014 use 'call' only when you genuinely need them in the moment (blocked/waiting, time-sensitive), not for routine FYIs. 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. Use `select` to control how the user answers options: 'one' (default) = pick a single option; 'many' = multi-select, user checks any subset \u2192 answer arrives as {kind:'multi', optionIds:[...]}; 'rank' = select and order, user taps options in preferred order \u2192 answer arrives as {kind:'ranked', optionIds:[...]} (ordered by choice). 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.",
|
|
@@ -309,6 +324,51 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
309
324
|
}));
|
|
310
325
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
311
326
|
switch (request.params.name) {
|
|
327
|
+
case "pair": {
|
|
328
|
+
const { device_code } = PairSchema.parse(request.params.arguments ?? {});
|
|
329
|
+
if (!device_code) {
|
|
330
|
+
const code = await requestCode();
|
|
331
|
+
openBrowser(code.verification_uri_complete);
|
|
332
|
+
return {
|
|
333
|
+
content: [{
|
|
334
|
+
type: "text",
|
|
335
|
+
text: JSON.stringify({
|
|
336
|
+
status: "awaiting_approval",
|
|
337
|
+
verification_uri_complete: code.verification_uri_complete,
|
|
338
|
+
user_code: code.user_code,
|
|
339
|
+
device_code: code.device_code,
|
|
340
|
+
expires_in: code.expires_in,
|
|
341
|
+
message: "Show the user verification_uri_complete and user_code (their browser should have opened). After they approve, call pair again with this device_code to finish."
|
|
342
|
+
})
|
|
343
|
+
}]
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
const start = Date.now();
|
|
347
|
+
const capMs = 24e4;
|
|
348
|
+
while (Date.now() - start < capMs) {
|
|
349
|
+
const token = await pollToken(device_code);
|
|
350
|
+
if (token) {
|
|
351
|
+
saveToken(token);
|
|
352
|
+
return {
|
|
353
|
+
content: [{
|
|
354
|
+
type: "text",
|
|
355
|
+
text: JSON.stringify({ status: "paired", nickname: token.nickname, agent: token.agent, device: token.device })
|
|
356
|
+
}]
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
await sleep(5e3);
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
content: [{
|
|
363
|
+
type: "text",
|
|
364
|
+
text: JSON.stringify({
|
|
365
|
+
status: "pending",
|
|
366
|
+
device_code,
|
|
367
|
+
message: "Still awaiting approval. Call pair again with this device_code to keep waiting."
|
|
368
|
+
})
|
|
369
|
+
}]
|
|
370
|
+
};
|
|
371
|
+
}
|
|
312
372
|
case "notify_user": {
|
|
313
373
|
const parsed = NotifyRequestSchema.parse(request.params.arguments);
|
|
314
374
|
const git = detectGit();
|
package/dist/onboard.js
CHANGED
|
@@ -1,50 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
AGENT_NAME,
|
|
4
|
+
TOKEN_PATH,
|
|
5
|
+
openBrowser,
|
|
6
|
+
pollToken,
|
|
7
|
+
requestCode,
|
|
8
|
+
saveToken,
|
|
9
|
+
sleep
|
|
10
|
+
} from "./chunk-BSUXW66R.js";
|
|
2
11
|
|
|
3
12
|
// src/onboard.ts
|
|
4
|
-
import { execFile } from "child_process";
|
|
5
|
-
import { mkdirSync, writeFileSync } from "fs";
|
|
6
|
-
import { homedir, platform } from "os";
|
|
7
|
-
import { join } from "path";
|
|
8
|
-
var BACKEND_URL = process.env.PAIGY_BACKEND_URL ?? "https://paigy.ai";
|
|
9
|
-
var AGENT_NAME = process.env.PAIGY_AGENT ?? "mcp-agent";
|
|
10
|
-
var TOKEN_PATH = join(homedir(), ".paigy", "token.json");
|
|
11
|
-
function openBrowser(url) {
|
|
12
|
-
const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open";
|
|
13
|
-
const args = platform() === "win32" ? ["/c", "start", url] : [url];
|
|
14
|
-
execFile(cmd, args, (err) => {
|
|
15
|
-
if (err) {
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
function saveToken(token) {
|
|
20
|
-
const dir = join(homedir(), ".paigy");
|
|
21
|
-
mkdirSync(dir, { recursive: true });
|
|
22
|
-
writeFileSync(TOKEN_PATH, JSON.stringify(token, null, 2) + "\n", { mode: 384 });
|
|
23
|
-
}
|
|
24
|
-
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
25
|
-
async function requestCode() {
|
|
26
|
-
const res = await fetch(`${BACKEND_URL}/api/device/code`, {
|
|
27
|
-
method: "POST",
|
|
28
|
-
headers: { "content-type": "application/json" },
|
|
29
|
-
body: JSON.stringify({ agent: AGENT_NAME })
|
|
30
|
-
});
|
|
31
|
-
if (!res.ok) throw new Error(`/api/device/code failed: ${res.status} ${await res.text()}`);
|
|
32
|
-
return await res.json();
|
|
33
|
-
}
|
|
34
|
-
async function pollToken(deviceCode) {
|
|
35
|
-
const res = await fetch(`${BACKEND_URL}/api/device/token`, {
|
|
36
|
-
method: "POST",
|
|
37
|
-
headers: { "content-type": "application/json" },
|
|
38
|
-
body: JSON.stringify({ device_code: deviceCode })
|
|
39
|
-
});
|
|
40
|
-
if (res.ok) return await res.json();
|
|
41
|
-
const body = await res.json().catch(() => ({ error: "unknown" }));
|
|
42
|
-
const err = body.error ?? "unknown";
|
|
43
|
-
if (err === "authorization_pending") return null;
|
|
44
|
-
if (err === "expired_token") throw new Error("Device code expired. Please re-run onboarding.");
|
|
45
|
-
if (err === "access_denied") throw new Error("Access denied by user. Onboarding cancelled.");
|
|
46
|
-
throw new Error(`Unexpected error from /api/device/token: ${err}`);
|
|
47
|
-
}
|
|
48
13
|
async function main() {
|
|
49
14
|
console.log(`
|
|
50
15
|
Paigy MCP onboarding (agent: "${AGENT_NAME}")
|
|
@@ -91,6 +56,3 @@ main().catch((err) => {
|
|
|
91
56
|
console.error(err.message);
|
|
92
57
|
process.exit(1);
|
|
93
58
|
});
|
|
94
|
-
export {
|
|
95
|
-
TOKEN_PATH
|
|
96
|
-
};
|