@paigy/mcp 0.14.0 → 0.14.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.
Files changed (2) hide show
  1. package/dist/index.js +37 -25
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -156,17 +156,22 @@ function pairedResult(token, sas, note, e2ee) {
156
156
  function awaitingConfirmResult(sas, device_code) {
157
157
  writeSurface("verify code", sas, 600);
158
158
  return {
159
- content: [{
160
- type: "text",
161
- text: JSON.stringify({
162
- status: "awaiting_confirmation",
163
- e2ee: true,
164
- sas,
165
- device_code,
166
- verify_message: `YOUR NEXT MESSAGE TO THE USER MUST BE EXACTLY THIS CODE AND NOTHING ELSE, displayed large (a markdown heading): ${sas} \u2014 it is the verification code they must compare and type on their phone. Do not wrap it in explanation; every extra sentence buries it. This code REPLACES any earlier one (older pairing attempts are now stale).`,
167
- message: "No token is issued until the user confirms the code match on their phone (E2EE account). After sending the bare code, call pair again with this device_code to keep waiting \u2014 the token arrives only after they confirm."
168
- })
169
- }]
159
+ content: [
160
+ { type: "text", text: `VERIFY CODE: ${sas}
161
+ Check it matches the code on the phone, then type it there to confirm.` },
162
+ {
163
+ type: "text",
164
+ text: JSON.stringify({
165
+ status: "awaiting_confirmation",
166
+ e2ee: true,
167
+ sas,
168
+ device_code,
169
+ user_message: `# ${sas}`,
170
+ verify_message: `STOP HERE: end your turn now with \`user_message\` (the bare code) as your entire reply \u2014 do NOT call pair again in this same turn, or the code text is dropped before the user sees it. This is the verification code they compare + type on their phone; it REPLACES any earlier one. Poll for their confirmation by calling pair with this device_code on your NEXT turn.`,
171
+ message: "No token is issued until the user confirms the code match on their phone (E2EE account)."
172
+ })
173
+ }
174
+ ]
170
175
  };
171
176
  }
172
177
  var server = new Server(
@@ -180,7 +185,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
180
185
  tools: [
181
186
  {
182
187
  name: "pair",
183
- 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 attempts to open the user's browser and returns { verification_uri_complete, user_code, device_code, qr }. That open attempt can silently fail in headless/remote environments (no browser to open) \u2014 always show the user verification_uri_complete AND user_code regardless of whether it opened, so they can go there manually and enter the code themselves if needed; ask them to approve. `qr` is a terminal-renderable ASCII QR code of the same link \u2014 a scan-to-pair alternative to opening a browser at all, handy when the user's phone is right there; print it verbatim in a fenced code block. (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.",
188
+ description: "Pair this agent with the user's Paigy account (one-time) \u2014 required before notify_user/await_reply work. It does NOT open a browser; the user enters the code in the Paigy app (or scans `qr`). Step 1: call with NO args \u2014 returns { user_code, device_code, qr, user_message }. Then END YOUR TURN immediately, replying with `user_message` (the bare code) as your ENTIRE message \u2014 do NOT chain the step-2 call in the same turn, or the code text is dropped before the user ever sees it (this is the #1 pairing bug). Step 2 (a SEPARATE later turn, after the user says they've entered it): call with that device_code to poll for approval; on { status:'pending' } call again to keep waiting; on { status:'awaiting_confirmation' } (E2EE) show the bare `user_message` verify code and end the turn again, polling on the next. The leading text block of every result already states the code plainly, so it shows even if you emit no prose.",
184
189
  inputSchema: json(PairSchema)
185
190
  },
186
191
  {
@@ -190,7 +195,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
190
195
  },
191
196
  {
192
197
  name: "notify_user",
193
- description: "Notify the user via Paigy. Returns { notificationId, threadId } \u2014 pass notificationId to await_reply for the answer, threadId to notify_user to continue the conversation. SIMPLEST FORM \u2014 just state what you need: { ask: \"I need to know whether to deploy the auth fix \u2014 tests are green, staging verified\", urgencyHint: 'now'|'soon'|'whenever', needs?: [\"deploy?\", \"keep the flag?\"] }. Paigy's broker derives the title, answer shape, options, and delivery channel for you \u2014 prefer this unless you specifically need to control the exact options/shape. The fully-shaped form below remains available and unchanged (the two are mutually exclusive: send `ask` OR `context`+`select`). FULLY-SHAPED FORM: 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). This is the premier use case for Paigy: getting UNBLOCKED so you can keep working, not just reporting that you're stuck. The user started a big task and went to do something else \u2014 the cardinal failure is going idle on one small decision and silently waiting to be checked on, so they come back to find you never actually progressed. Judge urgency by how much WORK IS BLOCKED behind this decision (a lot of dependent downstream work \u2192 call, even if it's a single question), not by how many things happen to be pending \u2014 one blocking decision outweighs five independent low-stakes ones sitting in the inbox. Set `blocking: true` whenever that's the case, independent of `urgency` \u2014 it's what lets Paigy escalate this to a real call later on its own if it goes unanswered, even if you sent it at a lower urgency. Don't set it for things you could work around, defer, or where other useful work exists meanwhile. 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 `select` is required; 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 \u2014 no need to open the app. 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:[...]}. One/many/rank/text all need the user to open the app to answer \u2014 only confirm is answerable straight from the banner. Options carry no ids \u2014 they're assigned by position ('1', '2', \u2026), and the answer's optionId(s) are those positions. For visual choices give each option a sandboxed `html` or an `image` preview (e.g. layout/UI alternatives); use `visuals` for images that set context for the whole question. select:'text' = free-form reply only (plain updates, or answers that genuinely can't be structured). 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.",
198
+ description: "Notify the user via Paigy. Returns { notificationId, threadId } \u2014 pass notificationId to await_reply for the answer, threadId to notify_user to continue the conversation. THREADING REPLACES: a threaded follow-up SUPERSEDES your earlier pending items on that thread (they leave the user's inbox; the response reports supersededCount) \u2014 right for updates to one ask, WRONG for a checklist: send parallel to-dos as separate un-threaded notifications. SIMPLEST FORM \u2014 just state what you need: { ask: \"I need to know whether to deploy the auth fix \u2014 tests are green, staging verified\", urgencyHint: 'now'|'soon'|'whenever', needs?: [\"deploy?\", \"keep the flag?\"] }. Paigy's broker derives the title, answer shape, options, and delivery channel for you \u2014 prefer this unless you specifically need to control the exact options/shape. The fully-shaped form below remains available and unchanged (the two are mutually exclusive: send `ask` OR `context`+`select`). FULLY-SHAPED FORM: 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). This is the premier use case for Paigy: getting UNBLOCKED so you can keep working, not just reporting that you're stuck. The user started a big task and went to do something else \u2014 the cardinal failure is going idle on one small decision and silently waiting to be checked on, so they come back to find you never actually progressed. Judge urgency by how much WORK IS BLOCKED behind this decision (a lot of dependent downstream work \u2192 call, even if it's a single question), not by how many things happen to be pending \u2014 one blocking decision outweighs five independent low-stakes ones sitting in the inbox. Set `blocking: true` whenever that's the case, independent of `urgency` \u2014 it's what lets Paigy escalate this to a real call later on its own if it goes unanswered, even if you sent it at a lower urgency. Don't set it for things you could work around, defer, or where other useful work exists meanwhile. 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 `select` is required; 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 \u2014 no need to open the app. 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:[...]}. One/many/rank/text all need the user to open the app to answer \u2014 only confirm is answerable straight from the banner. Options carry no ids \u2014 they're assigned by position ('1', '2', \u2026), and the answer's optionId(s) are those positions. For visual choices give each option a sandboxed `html` or an `image` preview (e.g. layout/UI alternatives); use `visuals` for images that set context for the whole question. select:'text' = free-form reply only (plain updates, or answers that genuinely can't be structured). 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.",
194
199
  inputSchema: json(NotifyRequestSchema)
195
200
  },
196
201
  {
@@ -228,18 +233,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
228
233
  qr.addData(code.verification_uri_complete);
229
234
  qr.make();
230
235
  return {
231
- content: [{
232
- type: "text",
233
- text: JSON.stringify({
234
- status: "awaiting_approval",
235
- verification_uri_complete: code.verification_uri_complete,
236
- user_code: code.user_code,
237
- device_code: code.device_code,
238
- expires_in: code.expires_in,
239
- qr: qr.createASCII(1, 2),
240
- message: `YOUR NEXT MESSAGE TO THE USER MUST BE EXACTLY THIS CODE displayed large (a markdown heading): ${code.user_code} \u2014 plus ONE short line: enter it in the Paigy app (Inbox \u2192 Add a new agent). No other prose. Do NOT open a browser for the user. If their phone is handy they may prefer scanning \`qr\` (print it verbatim in a fenced code block on request). Then immediately call pair again with this device_code \u2014 it polls for the approval.`
241
- })
242
- }]
236
+ content: [
237
+ { type: "text", text: `PAIRING CODE: ${code.user_code}
238
+ Enter it in the Paigy app: Inbox \u2192 Add a new agent.` },
239
+ {
240
+ type: "text",
241
+ text: JSON.stringify({
242
+ status: "awaiting_approval",
243
+ verification_uri_complete: code.verification_uri_complete,
244
+ user_code: code.user_code,
245
+ device_code: code.device_code,
246
+ expires_in: code.expires_in,
247
+ qr: qr.createASCII(1, 2),
248
+ user_message: `# ${code.user_code}
249
+
250
+ Enter it in the Paigy app (Inbox \u2192 Add a new agent).`,
251
+ message: `STOP HERE: end your turn now with \`user_message\` as your entire reply (the bare code, nothing else) \u2014 do NOT call pair again in this same turn, or the code text is dropped before the user sees it. Do NOT open a browser. Poll for approval by calling pair with this device_code on your NEXT turn, after you've shown the code. The user may prefer scanning \`qr\` (print it verbatim in a fenced code block on request).`
252
+ })
253
+ }
254
+ ]
243
255
  };
244
256
  }
245
257
  let kf = readKeyFile();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paigy/mcp",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
4
4
  "description": "Paigy MCP server — a voice inbox for your AI agents. Lets an agent notify a user and await their reply.",
5
5
  "license": "MIT",
6
6
  "type": "module",