@agentpickup/mcp 0.4.0 → 0.4.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 CHANGED
@@ -17,6 +17,7 @@ QR they show at the counter to collect.
17
17
  | `start_group_order` | Starts a group order (a coffee run) with a closing time; returns the code to share and a key to close it early |
18
18
  | `join_group_order` | Adds the user's items to a group order by its code; pay the share with `pay_order` before it closes |
19
19
  | `group_order_status` | Who's in, what they ordered, who has paid, and whether the shop's order is ready or collected |
20
+ | `share_agentpickup` | A QR and link for friends to add agentPickup to their assistant (as a card where the chat shows cards), for everyone or with one café picked |
20
21
  | `close_group_order` | Closes a group order early, as its organiser; the shop gets one combined order and the organiser its QR |
21
22
 
22
23
  ## The pickup pass card
@@ -26,7 +27,8 @@ among them), `create_pickup_order`, `order_status` and `get_pickup_pass` show th
26
27
  it's unpaid, then the QR and pickup code, kept up to date until it's collected. When the shop marks it ready, the QR's
27
28
  tile turns lime. The card is one self-contained page (`ui://agentpickup/pass-<version>.html`) that loads nothing from
28
29
  anywhere else. In apps without cards, the agent gives the user the pass link and pickup code. The shop emails the pass
29
- either way.
30
+ either way. The card's "Invite a friend" link, and `share_agentpickup`'s own card, point at the add page
31
+ (`addPageUrl`, default https://agentpickup.app/add) so friends can add it too.
30
32
 
31
33
  ## Use it
32
34
 
package/dist/card.d.ts CHANGED
@@ -20,6 +20,8 @@ export interface PassCard {
20
20
  pickupCode?: string | null;
21
21
  /** The QR as SVG, ink on transparent, so the tile behind it sets the colour. */
22
22
  qrSvg?: string;
23
+ /** The add page's share mode, for "Invite a friend". */
24
+ inviteUrl?: string;
23
25
  }
24
26
  /** The card's page. Its version goes in the handshake, so a host's logs say which card it ran. */
25
27
  export declare function passCardHtml(version: string): string;
package/dist/card.js CHANGED
@@ -54,7 +54,8 @@ body{font-family:var(--font-sans,ui-sans-serif,system-ui,-apple-system,"Segoe UI
54
54
  button{font:inherit;font-size:14px;border-radius:10px;padding:10px 14px;cursor:pointer;border:1px solid var(--line);background:var(--surface);color:var(--text)}
55
55
  button.pay{background:var(--lime);color:var(--ink);border-color:var(--lime);font-weight:600}
56
56
  button.link{background:none;border:none;padding:0;color:var(--muted);text-decoration:underline;font-size:13px}
57
- .foot{font-size:11px;color:var(--muted);margin-top:12px;text-align:right}
57
+ .foot{font-size:11px;color:var(--muted);margin-top:12px;display:flex;justify-content:space-between;align-items:center;gap:8px}
58
+ .foot button.link{font-size:12px}
58
59
  @media (prefers-reduced-motion:reduce){.pour{transition:none}.wrap.ready::after{animation:none;opacity:1}}
59
60
  </style>
60
61
  </head>
@@ -221,7 +222,7 @@ button.link{background:none;border:none;padding:0;color:var(--muted);text-decora
221
222
  if (status === "awaiting_payment" && s.checkoutUrl) actions += '<button class="pay" data-open="checkoutUrl">Pay' + (s.total ? " " + esc(s.total) : "") + " by card</button>";
222
223
  if (live && s.passUrl) actions += '<button class="link" data-open="passUrl">Open the pass</button>';
223
224
  if (actions) h += '<div class="row">' + actions + "</div>";
224
- h += '<div class="foot">agentPickup</div></div>';
225
+ h += '<div class="foot">' + (s.inviteUrl ? '<button class="link" data-open="inviteUrl">Invite a friend</button>' : "<span></span>") + "<span>agentPickup</span></div></div>";
225
226
  return h;
226
227
  }
227
228
  function render() {
package/dist/server.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { type Fetch, type Payer } from "./wallet.js";
3
- export declare const VERSION = "0.4.0";
3
+ export declare const VERSION = "0.4.1";
4
4
  export interface PickupMcpOptions {
5
5
  /** The agentPickup shop this server orders from. */
6
6
  shopUrl: string;
@@ -22,6 +22,11 @@ export interface PickupMcpOptions {
22
22
  payer?: Payer;
23
23
  /** Notes added to the instructions agents read on connecting, e.g. which of this host's shops take what. */
24
24
  instructions?: string;
25
+ /**
26
+ * The page where people add agentPickup to their assistant, which share_agentpickup and the pass card's
27
+ * "Invite a friend" link point to. Default https://agentpickup.app/add.
28
+ */
29
+ addPageUrl?: string;
25
30
  }
26
31
  export declare function createPickupMcpServer(opts: PickupMcpOptions): McpServer;
27
32
  /** A Streamable HTTP endpoint (stateless, JSON responses) for hosting the server, e.g. at /mcp. */
package/dist/server.js CHANGED
@@ -5,11 +5,14 @@ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/
5
5
  import QRCode from "qrcode";
6
6
  import { z } from "zod";
7
7
  import { CARD_META_KEY, CARD_MIME_TYPE, passCardHtml } from "./card.js";
8
+ import { SHARE_META_KEY, shareCardHtml } from "./share-card.js";
8
9
  import { errorOf, usd } from "./wallet.js";
9
- export const VERSION = "0.4.0";
10
+ export const VERSION = "0.4.1";
10
11
  // The pickup pass card (MCP Apps). The version is in the URI so hosts that cache cards pick up a new one.
11
12
  const CARD_URI = `ui://agentpickup/pass-${VERSION}.html`;
12
13
  const CARD_TOOL_META = { ui: { resourceUri: CARD_URI }, "ui/resourceUri": CARD_URI };
14
+ const SHARE_URI = `ui://agentpickup/share-${VERSION}.html`;
15
+ const SHARE_TOOL_META = { ui: { resourceUri: SHARE_URI }, "ui/resourceUri": SHARE_URI };
13
16
  // Shop names for the card, so polling order_status needn't read the catalog each time. Shared across the
14
17
  // per-request servers a hosted endpoint makes.
15
18
  const shopNames = new Map();
@@ -20,6 +23,8 @@ const rememberShop = (shop, name) => {
20
23
  shopNames.clear();
21
24
  shopNames.set(shop, name);
22
25
  };
26
+ /** A shop's name as the add page's ?shop= matches it: "Demo Café" -> "demo-cafe". */
27
+ const slugOf = (name) => name.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
23
28
  /** The QR in ink on transparent: the card's tile behind it is white, or lime once the order is ready. */
24
29
  const qrSvg = (content) => QRCode.toString(content, { type: "svg", errorCorrectionLevel: "M", margin: 0, color: { dark: "#0a0b0dff", light: "#00000000" } });
25
30
  class ToolError extends Error {
@@ -37,6 +42,7 @@ const INSTRUCTIONS = `agentPickup lets you buy things online for the user to col
37
42
  once it's paid and keeps itself up to date until it's collected. Otherwise give the user the pass link and pickup
38
43
  code. They show the QR at the counter, or say the code, to collect.
39
44
  get_pickup_pass (with the claim) fetches the pass again; order_status (with the claim) says whether it's ready.
45
+ share_agentpickup: when the user asks how to share agentPickup or get friends using it, shows a QR and link.
40
46
  Group orders (a coffee run): start_group_order gives a code to share; each person's agent calls join_group_order and
41
47
  pays its own share with pay_order; at closing time the shop gets one combined order and the organiser gets its QR.
42
48
  group_order_status shows who's in; close_group_order closes it early.`;
@@ -123,10 +129,22 @@ export function createPickupMcpServer(opts) {
123
129
  total: money(order.totalCents, order.currency),
124
130
  collectBy: order.collectBy,
125
131
  collectedAt: order.collectedAt ?? null,
132
+ inviteUrl: addLinks(shop).sharePageUrl,
126
133
  ...extra,
127
134
  ...(pass ? { passUrl: pass.passUrl, pickupCode: pass.pickupCode, qrSvg: await qrSvg(pass.qrContent) } : {}),
128
135
  };
129
136
  }
137
+ /** The add page for sharing: for one of this host's shops when given (?shop=<slug>), else everyone's. */
138
+ const addPage = (opts.addPageUrl ?? "https://agentpickup.app/add").replace(/\/$/, "");
139
+ function addLinks(shop) {
140
+ let slug = "";
141
+ if (shop) {
142
+ const u = new URL(shop);
143
+ slug = u.pathname.match(/\/v\/([^/]+)/)?.[1] ?? (u.pathname === "/" || u.pathname === "" ? slugOf(shopNames.get(shop) ?? "") : "");
144
+ }
145
+ const q = slug ? `shop=${encodeURIComponent(slug)}` : "";
146
+ return { url: `${addPage}${q ? `?${q}` : ""}`, sharePageUrl: `${addPage}?share=1${q ? `&${q}` : ""}` };
147
+ }
130
148
  const claimArg = z
131
149
  .string()
132
150
  .max(200)
@@ -411,6 +429,43 @@ export function createPickupMcpServer(opts) {
411
429
  await shopName(shop);
412
430
  return reply(`Pickup pass: ${pass.passUrl}, pickup code ${pass.pickupCode}. The user shows the QR at the counter, or says the code.`, { passUrl: pass.passUrl, pickupCode: pass.pickupCode, qrContent: pass.qrContent, status: pass.order?.status }, pass.order ? await cardFor(shop, pass.order, { claim: args.claim }, pass) : undefined);
413
431
  }));
432
+ // ---------- sharing ----------
433
+ server.registerResource("share-agentpickup", SHARE_URI, {
434
+ title: "Share agentPickup",
435
+ description: "A QR and link for friends to add agentPickup to their assistant.",
436
+ mimeType: CARD_MIME_TYPE,
437
+ _meta: { ui: { prefersBorder: false } },
438
+ }, async () => ({
439
+ contents: [{ uri: SHARE_URI, mimeType: CARD_MIME_TYPE, text: shareCardHtml(VERSION), _meta: { ui: { prefersBorder: false } } }],
440
+ }));
441
+ server.registerTool("share_agentpickup", {
442
+ title: "Share agentPickup",
443
+ description: "A QR code and link the user can share so friends can add agentPickup to Claude, ChatGPT or Grok and order too. " +
444
+ "Use when the user asks how to share agentPickup, invite friends, or get others using it. Name a shop to share it with that café picked.",
445
+ inputSchema: { ...shopArg },
446
+ annotations: { title: "Share agentPickup", readOnlyHint: true, openWorldHint: false },
447
+ _meta: SHARE_TOOL_META,
448
+ }, guarded(async (args) => {
449
+ const shop = args.shop_url ? await shopFor(args.shop_url) : undefined;
450
+ const name = shop ? await shopName(shop) : undefined;
451
+ const links = addLinks(shop);
452
+ const card = {
453
+ v: 1,
454
+ ...links,
455
+ qrSvg: await QRCode.toString(links.url, { type: "svg", errorCorrectionLevel: "M", margin: 0, color: { dark: "#0a0b0dff", light: "#00000000" } }),
456
+ ...(name ? { shopName: name } : {}),
457
+ };
458
+ return {
459
+ content: [
460
+ {
461
+ type: "text",
462
+ text: `Share this link: ${links.url}. A friend opens it (or scans its QR off the user's screen), adds agentPickup to Claude, ChatGPT or Grok, ` +
463
+ `then just asks for their usual${name ? ` at ${name}` : ""}. ${links.sharePageUrl} opens the phone's share sheet and has a sticker to print.`,
464
+ },
465
+ ],
466
+ _meta: { [SHARE_META_KEY]: card },
467
+ };
468
+ }));
414
469
  const closesIn = (g) => {
415
470
  const minutes = Math.max(0, Math.round((g.closesAt - Date.now()) / 60_000));
416
471
  return g.status === "open" ? `closes in ${minutes} min` : g.status === "sent" ? "closed and sent to the shop" : "cancelled";
@@ -0,0 +1,13 @@
1
+ export declare const SHARE_META_KEY = "agentpickup/share";
2
+ export interface ShareCard {
3
+ v: 1;
4
+ /** The add page, maybe for one café (?shop=). */
5
+ url: string;
6
+ /** The add page's share mode: the share sheet and the sticker. */
7
+ sharePageUrl: string;
8
+ /** The QR as SVG, ink on transparent, for the white tile. */
9
+ qrSvg: string;
10
+ /** A café to name, when the link is for one. */
11
+ shopName?: string;
12
+ }
13
+ export declare function shareCardHtml(version: string): string;
@@ -0,0 +1,142 @@
1
+ // The share card (MCP Apps): what share_agentpickup shows when someone asks how to get friends using agentPickup.
2
+ // The counter sticker as a card: a QR to the add page that a friend scans off the screen, the link to copy, and
3
+ // buttons that open the add page's share mode (the phone's share sheet, and the printable sticker). Self-contained,
4
+ // like the pass card: no scripts, fonts or images from anywhere else, and the same small MCP Apps bridge.
5
+ export const SHARE_META_KEY = "agentpickup/share";
6
+ const PAGE = String.raw `<!doctype html>
7
+ <html lang="en">
8
+ <head>
9
+ <meta charset="utf-8">
10
+ <meta name="viewport" content="width=device-width,initial-scale=1">
11
+ <title>Share agentPickup</title>
12
+ <style>
13
+ :root{--bg:#0a0b0d;--text:#eef0f3;--muted:#b7bdc6;--dim:#8b929c;--line:#2a2f36;--lime:#b5f23d;--ink:#0a0b0d}
14
+ html,body{margin:0;padding:0;background:transparent}
15
+ body{font-family:var(--font-sans,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif);padding:2px;-webkit-font-smoothing:antialiased}
16
+ .card{box-sizing:border-box;max-width:420px;margin:0 auto;background:var(--bg);color:var(--text);border-radius:18px;padding:20px 20px 16px}
17
+ .top{display:flex;align-items:center;gap:8px;font-weight:700;font-size:15px}
18
+ .top svg{width:22px;height:22px;border-radius:5px}
19
+ .top .dim{color:var(--dim)}
20
+ h1{margin:14px 0 0;font-size:34px;line-height:.95;font-weight:850;letter-spacing:-.02em;text-transform:uppercase}
21
+ h1 em{font-style:normal;color:var(--lime)}
22
+ .sub{margin:8px 0 0;color:var(--muted);font-size:15px}
23
+ .row{display:flex;gap:16px;align-items:flex-end;margin-top:16px}
24
+ .tile{background:#fff;border-radius:14px;padding:12px;width:156px;height:156px;flex:none;box-sizing:border-box}
25
+ .tile svg{display:block;width:100%;height:100%}
26
+ ol{list-style:none;margin:0;padding:0;counter-reset:n;color:var(--muted);font-size:15px}
27
+ li{counter-increment:n;margin:0 0 8px;display:flex;align-items:center;gap:8px}
28
+ li::before{content:counter(n);width:22px;height:22px;border-radius:50%;border:1.5px solid var(--line);display:inline-flex;align-items:center;justify-content:center;font:600 12px ui-monospace,Menlo,monospace;color:var(--lime);flex:none}
29
+ .link{margin-top:14px;display:flex;gap:8px;align-items:center}
30
+ .link code{flex:1;min-width:0;overflow-wrap:anywhere;font:13px ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text);background:#16191d;border:1px solid var(--line);border-radius:10px;padding:9px 10px;user-select:all;-webkit-user-select:all}
31
+ button{font-weight:600;font-size:14px;font-family:inherit;border-radius:10px;padding:10px 14px;cursor:pointer;border:1px solid var(--line);background:transparent;color:var(--text);white-space:nowrap}
32
+ button.lime{background:var(--lime);color:var(--ink);border-color:var(--lime)}
33
+ .actions{display:flex;gap:8px;margin-top:10px;flex-wrap:wrap}
34
+ .actions button{flex:1}
35
+ .note{margin:10px 0 0;font-size:12px;color:var(--dim)}
36
+ @media (max-width:360px){.row{flex-direction:column;align-items:flex-start}}
37
+ </style>
38
+ </head>
39
+ <body>
40
+ <div id="root"></div>
41
+ <script>
42
+ (function () {
43
+ var VERSION = "__VERSION__";
44
+ var root = document.getElementById("root");
45
+ var state = null, canOpen = false, nextId = 1, pending = {}, lastHeight = -1;
46
+ var host = window.parent;
47
+ function send(msg) { try { host.postMessage(msg, "*"); } catch (e) {} }
48
+ function request(method, params) {
49
+ var id = nextId++;
50
+ send({ jsonrpc: "2.0", id: id, method: method, params: params });
51
+ return new Promise(function (resolve, reject) {
52
+ pending[id] = { resolve: resolve, reject: reject };
53
+ setTimeout(function () { if (pending[id]) { delete pending[id]; reject(new Error("timed out")); } }, 30000);
54
+ });
55
+ }
56
+ function notify(method, params) { send({ jsonrpc: "2.0", method: method, params: params }); }
57
+ window.addEventListener("message", function (e) {
58
+ if (host !== window && e.source !== host) return;
59
+ var m = e.data;
60
+ if (!m || m.jsonrpc !== "2.0") return;
61
+ if (m.id != null && !m.method) {
62
+ var p = pending[m.id];
63
+ if (p) { delete pending[m.id]; if (m.error) p.reject(m.error); else p.resolve(m.result); }
64
+ return;
65
+ }
66
+ if (m.method === "ui/notifications/tool-result") return onResult(m.params);
67
+ if (m.method === "ui/notifications/host-context-changed") { applyContext(m.params || {}); return; }
68
+ if (m.method === "ui/resource-teardown" && m.id != null) return send({ jsonrpc: "2.0", id: m.id, result: {} });
69
+ if (m.method === "ping" && m.id != null) return send({ jsonrpc: "2.0", id: m.id, result: {} });
70
+ if (m.id != null && m.method && m.method.indexOf("ui/notifications/") !== 0) send({ jsonrpc: "2.0", id: m.id, error: { code: -32601, message: "Method not found" } });
71
+ });
72
+ function applyContext(ctx) {
73
+ var vars = ctx.styles && ctx.styles.variables;
74
+ if (vars && typeof vars["--font-sans"] === "string") document.documentElement.style.setProperty("--font-sans", vars["--font-sans"]);
75
+ }
76
+ function openLink(url) {
77
+ if (!url) return;
78
+ if (canOpen) request("ui/open-link", { url: url }).catch(function () { window.open(url, "_blank", "noopener"); });
79
+ else window.open(url, "_blank", "noopener");
80
+ }
81
+ function onResult(result) {
82
+ var data = result && result._meta && result._meta["agentpickup/share"];
83
+ if (data && data.url) state = data;
84
+ render();
85
+ }
86
+ function esc(s) { return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) { return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]; }); }
87
+ function safeSvg(svg) { return typeof svg === "string" && /^<svg[\s>]/.test(svg) && !/<script|<foreignObject|\son[a-z]+=/i.test(svg) ? svg : ""; }
88
+ function copy(text, button) {
89
+ var done = function () { button.textContent = "Copied"; setTimeout(function () { button.textContent = "Copy"; }, 1600); };
90
+ try {
91
+ if (navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(text).then(done, function () { select(); });
92
+ } catch (e) {}
93
+ select();
94
+ }
95
+ function select() {
96
+ var code = document.getElementById("url");
97
+ if (!code || !window.getSelection) return;
98
+ var range = document.createRange();
99
+ range.selectNodeContents(code);
100
+ var sel = window.getSelection();
101
+ sel.removeAllRanges();
102
+ sel.addRange(range);
103
+ }
104
+ function render() {
105
+ if (!state) { root.innerHTML = ""; return reportSize(); }
106
+ var svg = safeSvg(state.qrSvg);
107
+ var shown = state.url.replace(/^https?:\/\//, "");
108
+ root.innerHTML =
109
+ '<div class="card">' +
110
+ '<div class="top"><svg viewBox="0 0 32 32" aria-hidden="true"><rect width="32" height="32" fill="#16191d"/><rect x="7" y="7" width="7" height="7" rx="1.5" fill="#fff"/><rect x="18" y="7" width="7" height="7" rx="1.5" fill="#fff"/><rect x="7" y="18" width="7" height="7" rx="1.5" fill="#fff"/><path d="M18.5 21.5l2.5 2.5 4.5-5" fill="none" stroke="#b5f23d" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg><span><span class="dim">agent</span>Pickup</span></div>' +
111
+ "<h1>" + (state.shopName ? "Order from " + esc(state.shopName) + "<br>with your <em>AI</em>" : "Order ahead<br>from your <em>AI</em>") + "</h1>" +
112
+ '<p class="sub">Claude, ChatGPT or Grok</p>' +
113
+ '<div class="row">' + (svg ? '<div class="tile">' + svg.replace("<svg", '<svg role="img" aria-label="QR code to add agentPickup"') + "</div>" : "") +
114
+ "<ol><li>Scan</li><li>Add agentPickup</li><li>Ask for your usual</li></ol></div>" +
115
+ '<div class="link"><code id="url">' + esc(shown) + '</code><button type="button" id="copy">Copy</button></div>' +
116
+ '<div class="actions"><button class="lime" type="button" id="share">Share</button><button type="button" id="sticker">Get the sticker</button></div>' +
117
+ '<p class="note">A friend scans the QR off your screen, or you send them the link.</p>' +
118
+ "</div>";
119
+ document.getElementById("copy").addEventListener("click", function (e) { copy(state.url, e.currentTarget); });
120
+ document.getElementById("share").addEventListener("click", function () { openLink(state.sharePageUrl); });
121
+ document.getElementById("sticker").addEventListener("click", function () { openLink(state.sharePageUrl + (state.sharePageUrl.indexOf("?") >= 0 ? "&" : "?") + "sticker=1"); });
122
+ reportSize();
123
+ }
124
+ function reportSize() {
125
+ var height = Math.ceil(document.body.getBoundingClientRect().height);
126
+ if (height !== lastHeight) { lastHeight = height; notify("ui/notifications/size-changed", { height: height }); }
127
+ }
128
+ if (window.ResizeObserver) new ResizeObserver(reportSize).observe(document.body);
129
+ request("ui/initialize", { appInfo: { name: "agentpickup-share", version: VERSION }, appCapabilities: {}, protocolVersion: "2026-01-26" })
130
+ .then(function (r) {
131
+ canOpen = Boolean(r && r.hostCapabilities && r.hostCapabilities.openLinks);
132
+ applyContext((r && r.hostContext) || {});
133
+ notify("ui/notifications/initialized", {});
134
+ render();
135
+ }, function () { render(); });
136
+ })();
137
+ </script>
138
+ </body>
139
+ </html>`;
140
+ export function shareCardHtml(version) {
141
+ return PAGE.replace("__VERSION__", version);
142
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentpickup/mcp",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "MCP server for agentPickup: let an AI agent find pickup shops near its user, order, pay by card or in USDC over x402, and hand its user the QR to collect in person.",
5
5
  "mcpName": "io.github.dav3trad3r/agentpickup",
6
6
  "type": "module",