@agentpickup/mcp 0.4.3 → 0.4.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/dist/card.d.ts CHANGED
@@ -1,27 +1,47 @@
1
1
  export declare const CARD_MIME_TYPE = "text/html;profile=mcp-app";
2
- /** Where tools hand their card data: the card reads it, the model doesn't need it. */
2
+ /** The _meta key that mirrors structuredContent.card, for hosts that pass _meta. */
3
3
  export declare const CARD_META_KEY = "agentpickup/pass";
4
+ /** A QR as a bit grid: size × size modules, row by row, packed 8 to a byte (first module in the top bit), base64. */
5
+ export interface QrBits {
6
+ size: number;
7
+ bits: string;
8
+ }
4
9
  /** What the card draws. Every field but orderId is optional: later results fill in or update what's there. */
5
10
  export interface PassCard {
6
11
  v: 1;
7
12
  orderId: string;
8
13
  shopUrl?: string;
9
14
  shopName?: string;
15
+ /** Where to collect: the shop's address, or its counter's name. */
16
+ where?: string;
10
17
  /** Lets the card call order_status itself while the order moves along. */
11
18
  claim?: string;
12
19
  status?: string;
13
20
  rail?: string;
21
+ /** "2× Flat white, 1× Croissant" */
14
22
  items?: string;
23
+ /** The order's lines with their prices, for the card's receipt while it's unpaid. */
24
+ lines?: {
25
+ qty: number;
26
+ name: string;
27
+ price?: string;
28
+ }[];
15
29
  total?: string;
16
30
  collectBy?: number | null;
17
31
  collectedAt?: number | null;
18
32
  checkoutUrl?: string;
19
33
  passUrl?: string;
20
34
  pickupCode?: string | null;
21
- /** The QR as SVG, ink on transparent, so the tile behind it sets the colour. */
22
- qrSvg?: string;
35
+ /** The pass's QR, once it's paid. */
36
+ qr?: QrBits;
23
37
  /** The add page's share mode, for "Invite a friend". */
24
38
  inviteUrl?: string;
25
39
  }
40
+ /**
41
+ * Draws a QR bit grid as an SVG path, one run of dark modules per segment. Plain JavaScript source, so the cards
42
+ * run exactly this text; the server and tests use the same function below.
43
+ */
44
+ export declare const QR_PATH_JS = "function (qr, decode) {\n var raw = (decode || function (b) { return atob(b); })(qr.bits);\n var size = qr.size;\n function dark(i) { return (raw.charCodeAt(i >> 3) >> (7 - (i & 7))) & 1; }\n var d = \"\";\n for (var y = 0; y < size; y++) {\n var x = 0;\n while (x < size) {\n if (!dark(y * size + x)) { x++; continue; }\n var start = x;\n while (x < size && dark(y * size + x)) x++;\n d += \"M\" + start + \" \" + y + \"h\" + (x - start) + \"v1h-\" + (x - start) + \"z\";\n }\n }\n return d;\n}";
45
+ export declare const qrPathFromBits: (qr: QrBits, decode?: (b64: string) => string) => string;
26
46
  /** The card's page. Its version goes in the handshake, so a host's logs say which card it ran. */
27
47
  export declare function passCardHtml(version: string): string;
package/dist/card.js CHANGED
@@ -1,14 +1,40 @@
1
1
  // The pickup pass as an MCP Apps card (ui:// resource, text/html;profile=mcp-app): chat apps that show cards (Claude,
2
- // ChatGPT) draw it beside the tool result. It shows the order, a Pay button while it's unpaid, then the QR and pickup
3
- // code, and keeps itself up to date (order_status with the order's claim) until it's collected. At "ready" the QR's
4
- // tile turns brand lime. One self-contained page: no scripts, fonts or images from anywhere else.
2
+ // ChatGPT) draw it beside the tool result. While the order is unpaid it shows what's in it and a Pay button; once
3
+ // paid, the QR, pickup code and collect-by time; and it keeps itself up to date (order_status with the order's
4
+ // claim) until it's collected. At "ready" the QR's tile turns brand lime. One self-contained page: no scripts,
5
+ // fonts or images from anywhere else.
6
+ //
7
+ // The card reads its data from the tool result's structuredContent.card. Hosts pass structuredContent to the card
8
+ // (Claude drops _meta), so everything the card draws goes there, with the QR as a compact bit grid rather than an
9
+ // image. _meta carries the same for hosts that read it, and the text JSON is a last resort.
5
10
  //
6
11
  // The bridge is the MCP Apps postMessage protocol (JSON-RPC 2.0 with the host, protocol 2026-01-26), written out
7
12
  // here rather than bundling the 300 KB SDK: ui/initialize, ui/notifications/initialized, tool-result,
8
13
  // host-context-changed, resource-teardown, tools/call, ui/open-link and ui/notifications/size-changed.
9
14
  export const CARD_MIME_TYPE = "text/html;profile=mcp-app";
10
- /** Where tools hand their card data: the card reads it, the model doesn't need it. */
15
+ /** The _meta key that mirrors structuredContent.card, for hosts that pass _meta. */
11
16
  export const CARD_META_KEY = "agentpickup/pass";
17
+ /**
18
+ * Draws a QR bit grid as an SVG path, one run of dark modules per segment. Plain JavaScript source, so the cards
19
+ * run exactly this text; the server and tests use the same function below.
20
+ */
21
+ export const QR_PATH_JS = `function (qr, decode) {
22
+ var raw = (decode || function (b) { return atob(b); })(qr.bits);
23
+ var size = qr.size;
24
+ function dark(i) { return (raw.charCodeAt(i >> 3) >> (7 - (i & 7))) & 1; }
25
+ var d = "";
26
+ for (var y = 0; y < size; y++) {
27
+ var x = 0;
28
+ while (x < size) {
29
+ if (!dark(y * size + x)) { x++; continue; }
30
+ var start = x;
31
+ while (x < size && dark(y * size + x)) x++;
32
+ d += "M" + start + " " + y + "h" + (x - start) + "v1h-" + (x - start) + "z";
33
+ }
34
+ }
35
+ return d;
36
+ }`;
37
+ export const qrPathFromBits = new Function(`return ${QR_PATH_JS}`)();
12
38
  const PAGE = String.raw `<!doctype html>
13
39
  <html lang="en">
14
40
  <head>
@@ -16,46 +42,53 @@ const PAGE = String.raw `<!doctype html>
16
42
  <meta name="viewport" content="width=device-width,initial-scale=1">
17
43
  <title>Pickup pass</title>
18
44
  <style>
19
- :root{--bg:#ffffff;--surface:#f4f4f1;--text:#16161a;--muted:#66666d;--line:rgba(10,11,13,.12);--ok:#2e7d32;--lime:#b5f23d;--ink:#0a0b0d;color-scheme:light}
20
- :root[data-theme=dark]{--bg:#1d1d20;--surface:#2a2a2e;--text:#f2f2f4;--muted:#a3a3ab;--line:rgba(255,255,255,.14);--ok:#8fd47f;color-scheme:dark}
21
- @media (prefers-color-scheme:dark){:root:not([data-theme]){--bg:#1d1d20;--surface:#2a2a2e;--text:#f2f2f4;--muted:#a3a3ab;--line:rgba(255,255,255,.14);--ok:#8fd47f;color-scheme:dark}}
45
+ :root{--bg:#ffffff;--surface:#f4f4f1;--text:#16161a;--muted:#66666d;--faint:#9a9aa0;--line:rgba(10,11,13,.1);--ok:#23843b;--okbg:#e8f5ea;--warn:#8a5a00;--warnbg:#fdf2dc;--lime:#b5f23d;--ink:#0a0b0d;color-scheme:light}
46
+ :root[data-theme=dark]{--bg:#1d1d20;--surface:#28282c;--text:#f2f2f4;--muted:#a3a3ab;--faint:#74747c;--line:rgba(255,255,255,.12);--ok:#8fd47f;--okbg:#1f3322;--warn:#f2c46b;--warnbg:#3a2f17;color-scheme:dark}
47
+ @media (prefers-color-scheme:dark){:root:not([data-theme]){--bg:#1d1d20;--surface:#28282c;--text:#f2f2f4;--muted:#a3a3ab;--faint:#74747c;--line:rgba(255,255,255,.12);--ok:#8fd47f;--okbg:#1f3322;--warn:#f2c46b;--warnbg:#3a2f17;color-scheme:dark}}
22
48
  html,body{margin:0;padding:0;background:transparent}
23
49
  body{font-family:var(--font-sans,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif);color:var(--text);-webkit-font-smoothing:antialiased;padding:2px}
24
- .card{box-sizing:border-box;max-width:380px;margin:0 auto;background:var(--bg);border:1px solid var(--line);border-radius:16px;padding:16px 18px}
25
- .head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:12px}
26
- .shop{font-weight:600;font-size:15px;margin:0}
27
- .items{font-size:13px;color:var(--muted);margin:2px 0 0}
28
- .badge{font-size:12px;line-height:1;padding:6px 10px;border-radius:999px;background:var(--surface);white-space:nowrap}
29
- .badge.ready{background:var(--lime);color:var(--ink);font-weight:600}
30
- .wrap{position:relative;border-radius:12px}
31
- .tile{position:relative;overflow:hidden;background:#ffffff;border-radius:12px;padding:14px;display:flex;justify-content:center;align-items:center;border:1px solid var(--line);min-height:120px}
32
- .tile svg{position:relative;display:block;width:100%;max-width:220px;height:auto}
50
+ .card{box-sizing:border-box;max-width:380px;margin:0 auto;background:var(--bg);border:1px solid var(--line);border-radius:18px;padding:16px 18px 14px}
51
+ .head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px}
52
+ .shop{font-weight:650;font-size:16px;margin:0;letter-spacing:-.01em}
53
+ .sub{font-size:13px;color:var(--muted);margin:2px 0 0}
54
+ .pill{font-size:12px;line-height:1;padding:6px 10px;border-radius:999px;white-space:nowrap;background:var(--surface);color:var(--muted)}
55
+ .pill.awaiting_payment{background:var(--warnbg);color:var(--warn)}
56
+ .pill.paid,.pill.collected{background:var(--okbg);color:var(--ok)}
57
+ .pill.ready{background:var(--lime);color:var(--ink);font-weight:650}
58
+ .lines{margin:14px 0 0;border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:8px 0;font-size:14px}
59
+ .line{display:flex;justify-content:space-between;gap:12px;padding:3px 0}
60
+ .line span:first-child{min-width:0;overflow-wrap:anywhere}
61
+ .line.total{font-weight:650}
62
+ .wrap{position:relative;border-radius:14px;margin-top:14px}
63
+ .tile{position:relative;overflow:hidden;background:#ffffff;border-radius:14px;padding:16px;display:flex;justify-content:center;align-items:center;border:1px solid var(--line);min-height:120px}
64
+ .tile svg{position:relative;display:block;width:100%;max-width:210px;height:auto}
33
65
  .pour{position:absolute;left:0;right:0;bottom:0;height:0;background:var(--lime);transition:height 1s cubic-bezier(.5,0,.2,1)}
34
66
  .wrap.ready .pour{height:100%}
35
67
  .wrap.ready .tile{border-color:var(--lime)}
36
- .wrap.ready::after{content:"";position:absolute;inset:0;border:2px solid var(--lime);border-radius:12px;animation:ring 1.8s ease-out infinite;pointer-events:none}
68
+ .wrap.ready::after{content:"";position:absolute;inset:0;border:2px solid var(--lime);border-radius:14px;animation:ring 1.8s ease-out infinite;pointer-events:none}
37
69
  @keyframes ring{from{transform:scale(1);opacity:.9}to{transform:scale(1.08);opacity:0}}
38
70
  .tile.plain{background:var(--surface)}
39
71
  .done{font-size:15px;color:var(--text);text-align:center}
40
- .done b{display:block;font-size:34px;line-height:1.2}
41
- .facts{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:10px;margin:12px 0 0}
42
- .fact{background:var(--surface);border-radius:10px;padding:8px 12px}
72
+ .done b{display:block;font-size:30px;line-height:1.2;color:var(--ok)}
73
+ .facts{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:8px;margin:10px 0 0}
74
+ .fact{background:var(--surface);border-radius:12px;padding:8px 12px}
43
75
  .fact span{display:block;font-size:12px;color:var(--muted)}
44
- .fact b{display:block;font-size:20px;font-weight:600;overflow-wrap:anywhere}
76
+ .fact b{display:block;font-size:19px;font-weight:650;overflow-wrap:anywhere}
45
77
  .code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;letter-spacing:2px}
46
- .steps{display:flex;justify-content:space-between;border-top:1px solid var(--line);padding-top:10px;margin-top:12px}
47
- .step{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted)}
48
- .dot{width:10px;height:10px;border-radius:50%;border:1.5px solid currentColor;box-sizing:border-box}
49
- .step.past{color:var(--ok)}.step.past .dot{background:currentColor}
50
- .step.now{color:var(--text);font-weight:600}.step.now .dot{background:var(--ok);border-color:var(--ok)}
78
+ .steps{display:flex;gap:6px;margin-top:14px}
79
+ .step{flex:1;display:flex;align-items:center;gap:6px;font-size:12px;color:var(--faint)}
80
+ .step i{width:9px;height:9px;border-radius:50%;border:1.5px solid currentColor;box-sizing:border-box;flex:none}
81
+ .step.past{color:var(--ok)}.step.past i{background:currentColor}
82
+ .step.now{color:var(--text);font-weight:650}.step.now i{background:var(--lime);border-color:var(--lime)}
51
83
  .msg{font-size:13px;color:var(--muted);margin:10px 0 0;line-height:1.45}
52
84
  .test{font-size:12px;color:var(--muted);margin:6px 0 0}
53
- .row{display:flex;gap:12px;align-items:center;margin-top:12px;flex-wrap:wrap}
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
- button.pay{background:var(--lime);color:var(--ink);border-color:var(--lime);font-weight:600}
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;display:flex;justify-content:space-between;align-items:center;gap:8px}
58
- .foot button.link{font-size:12px}
85
+ button{font:inherit;cursor:pointer}
86
+ .pay{display:block;width:100%;margin:14px 0 0;padding:13px 14px;border-radius:12px;border:0;background:var(--lime);color:var(--ink);font-size:15px;font-weight:650}
87
+ .note{font-size:12px;color:var(--muted);margin:8px 0 0;text-align:center;line-height:1.4}
88
+ .link{background:none;border:none;padding:0;color:var(--muted);text-decoration:underline;font-size:13px}
89
+ .actions{margin-top:10px}
90
+ .foot{font-size:11px;color:var(--faint);margin-top:14px;display:flex;justify-content:space-between;align-items:center;gap:8px}
91
+ .foot .link{font-size:12px;color:var(--muted)}
59
92
  @media (prefers-reduced-motion:reduce){.pour{transition:none}.wrap.ready::after{animation:none;opacity:1}}
60
93
  </style>
61
94
  </head>
@@ -69,6 +102,7 @@ button.link{background:none;border:none;padding:0;color:var(--muted);text-decora
69
102
  var root = document.getElementById("root");
70
103
  var state = null, canCall = false, canOpen = false, locale, timeZone;
71
104
  var nextId = 1, pending = {}, timer = null, started = Date.now(), lastHtml = "", poured = false, lastHeight = -1, stopped = false;
105
+ var qrPathFromBits = __QR_PATH__;
72
106
 
73
107
  // ---------- the bridge ----------
74
108
  var host = window.parent;
@@ -102,7 +136,7 @@ button.link{background:none;border:none;padding:0;color:var(--muted);text-decora
102
136
  function applyContext(ctx) {
103
137
  if (ctx.theme === "light" || ctx.theme === "dark") document.documentElement.setAttribute("data-theme", ctx.theme);
104
138
  var vars = ctx.styles && ctx.styles.variables;
105
- if (vars) for (var k in vars) if (k.indexOf("--") === 0 && typeof vars[k] === "string") document.documentElement.style.setProperty(k, vars[k]);
139
+ if (vars && typeof vars["--font-sans"] === "string") document.documentElement.style.setProperty("--font-sans", vars["--font-sans"]);
106
140
  if (ctx.locale) locale = ctx.locale;
107
141
  if (ctx.timeZone) timeZone = ctx.timeZone;
108
142
  }
@@ -115,15 +149,17 @@ button.link{background:none;border:none;padding:0;color:var(--muted);text-decora
115
149
  // ---------- results ----------
116
150
  function cardOf(result) {
117
151
  if (!result) return null;
152
+ var sc = result.structuredContent && result.structuredContent.card;
153
+ if (sc && sc.orderId) return sc;
118
154
  var meta = result._meta && result._meta["agentpickup/pass"];
119
155
  if (meta && meta.orderId) return meta;
120
- // A host that drops _meta: the JSON after the tool's summary line still has the basics.
156
+ // Neither came through: the JSON after the tool's summary line still has the basics.
121
157
  try {
122
158
  var text = result.content && result.content[0] && result.content[0].text;
123
159
  var data = JSON.parse(text.slice(text.indexOf("\n\n") + 2));
124
160
  var id = data.orderId || data.id || (data.order && data.order.id);
125
161
  if (!id) return null;
126
- return { orderId: id, status: data.status, passUrl: data.passUrl, pickupCode: data.pickupCode, checkoutUrl: data.checkoutUrl, total: data.total, claim: data.claim, shopUrl: data.shop };
162
+ return { orderId: id, status: data.status || (data.checkoutUrl || data.payUrl ? "awaiting_payment" : undefined), passUrl: data.passUrl, pickupCode: data.pickupCode, checkoutUrl: data.checkoutUrl, total: data.total, claim: data.claim, shopUrl: data.shop };
127
163
  } catch (e) { return null; }
128
164
  }
129
165
  function onResult(result) {
@@ -172,36 +208,59 @@ button.link{background:none;border:none;padding:0;color:var(--muted);text-decora
172
208
  return s;
173
209
  } catch (e) { return d.toLocaleTimeString(); }
174
210
  }
175
- function safeSvg(svg) { return typeof svg === "string" && /^<svg[\s>]/.test(svg) && !/<script|<foreignObject|\son[a-z]+=/i.test(svg) ? svg : ""; }
211
+ function qrSvg(qr) {
212
+ if (!qr || typeof qr.bits !== "string" || !(qr.size > 0 && qr.size < 200)) return "";
213
+ try {
214
+ return '<svg viewBox="0 0 ' + qr.size + " " + qr.size + '" shape-rendering="crispEdges" role="img" aria-label="Pickup QR code"><path fill="#0a0b0d" d="' + qrPathFromBits(qr) + '"/></svg>';
215
+ } catch (e) { return ""; }
216
+ }
176
217
  function message(s) {
177
218
  var live = canCall && s.claim;
178
219
  switch (s.status) {
179
- case "awaiting_payment":
180
- return s.checkoutUrl
181
- ? "Pay on the checkout page. " + (live ? "This card turns into your pickup pass once it's paid." : "Then ask for the pickup pass.")
182
- : "Waiting for the payment. " + (live ? "This card turns into your pickup pass once it's paid." : "");
220
+ case "awaiting_payment": return s.checkoutUrl ? "" : "Waiting for the payment. " + (live ? "This card becomes your pickup pass once it's paid." : "");
183
221
  case "paid": return "Paid. " + (live ? "This turns green when it's ready. " : "") + "The pass is in your email too.";
184
222
  case "ready": return "Ready at the counter. Show the QR or say the code.";
185
- case "collected": return s.collectedAt ? "Collected at " + time(s.collectedAt) + ". Enjoy." : "Collected. Enjoy.";
223
+ case "collected": return "Enjoy. The receipt is in your email.";
186
224
  case "expired": return "This order expired before anyone collected it.";
187
225
  case "cancelled": return "This order was cancelled.";
188
226
  default: return "";
189
227
  }
190
228
  }
229
+ function steps(status) {
230
+ var first = status === "awaiting_payment" ? "Pay" : "Paid";
231
+ var at = { awaiting_payment: 0, paid: 1, ready: 1, collected: 3 }[status];
232
+ if (at === undefined) return "";
233
+ return '<div class="steps">' + [first, "Ready", "Collected"].map(function (name, i) {
234
+ var cls = i < at || (status === "ready" && i === 1) ? " past" : i === at ? " now" : "";
235
+ if (status === "ready" && i === 2) cls = " now";
236
+ return '<span class="step' + cls + '"><i></i>' + name + "</span>";
237
+ }).join("") + "</div>";
238
+ }
191
239
  function view(s) {
192
240
  var status = s.status || "awaiting_payment";
193
- var label = LABELS[status] || status;
241
+ var unpaid = status === "awaiting_payment";
194
242
  var live = status === "paid" || status === "ready";
195
- var h = '<div class="card">';
196
- h += '<div class="head"><div><p class="shop">' + esc(s.shopName || "Your pickup order") + "</p>";
197
- if (s.items) h += '<p class="items">' + esc(s.items) + (s.total ? " · " + esc(s.total) : "") + "</p>";
198
- h += '</div><span class="badge' + (status === "ready" ? " ready" : "") + '">' + esc(label) + "</span></div>";
199
- var svg = safeSvg(s.qrSvg);
243
+ var h = '<div class="card"><div class="head"><div>';
244
+ h += '<p class="shop">' + esc(s.shopName || "Your pickup order") + "</p>";
245
+ var sub = unpaid ? (s.where ? "Pickup at " + s.where : "") : (s.items ? s.items + (s.total ? " · " + s.total : "") : "");
246
+ if (sub) h += '<p class="sub">' + esc(sub) + "</p>";
247
+ h += '</div><span class="pill ' + esc(status) + '">' + esc(LABELS[status] || status) + "</span></div>";
248
+
249
+ if (unpaid) {
250
+ var lines = s.lines && s.lines.length ? s.lines : null;
251
+ if (lines || s.total) {
252
+ h += '<div class="lines">';
253
+ if (lines) lines.forEach(function (l) { h += '<div class="line"><span>' + esc(l.qty + "× " + l.name) + "</span><span>" + esc(l.price || "") + "</span></div>"; });
254
+ else if (s.items) h += '<div class="line"><span>' + esc(s.items) + "</span><span></span></div>";
255
+ if (s.total) h += '<div class="line total"><span>Total</span><span>' + esc(s.total) + "</span></div>";
256
+ h += "</div>";
257
+ }
258
+ }
259
+ var svg = qrSvg(s.qr);
200
260
  if (live && svg) {
201
- h += '<div class="wrap' + (status === "ready" && poured ? " ready" : "") + '" id="wrap"><div class="tile"><div class="pour"></div>' +
202
- svg.replace("<svg", '<svg role="img" aria-label="Pickup QR code"') + "</div></div>";
261
+ h += '<div class="wrap' + (status === "ready" && poured ? " ready" : "") + '" id="wrap"><div class="tile"><div class="pour"></div>' + svg + "</div></div>";
203
262
  } else if (status === "collected") {
204
- h += '<div class="wrap"><div class="tile plain"><div class="done"><b>&#10003;</b>Collected</div></div></div>';
263
+ h += '<div class="wrap"><div class="tile plain"><div class="done"><b>&#10003;</b>' + esc(s.collectedAt ? "Collected at " + time(s.collectedAt) : "Collected") + "</div></div></div>";
205
264
  }
206
265
  if (live && (s.pickupCode || s.collectBy)) {
207
266
  h += '<div class="facts">';
@@ -209,19 +268,15 @@ button.link{background:none;border:none;padding:0;color:var(--muted);text-decora
209
268
  if (s.collectBy) h += '<div class="fact"><span>Collect by</span><b>' + esc(time(s.collectBy)) + "</b></div>";
210
269
  h += "</div>";
211
270
  }
212
- if (live || status === "collected") {
213
- var at = { paid: 0, ready: 1, collected: 2 }[status];
214
- h += '<div class="steps">' + ["Paid", "Ready", "Collected"].map(function (name, i) {
215
- return '<span class="step' + (i < at ? " past" : i === at ? " now" : "") + '"><span class="dot"></span>' + name + "</span>";
216
- }).join("") + "</div>";
217
- }
271
+ h += steps(status);
218
272
  var msg = message(s);
219
273
  if (msg) h += '<p class="msg">' + esc(msg) + "</p>";
274
+ if (unpaid && s.checkoutUrl) {
275
+ h += '<button class="pay" data-open="checkoutUrl">Pay' + (s.total ? " " + esc(s.total) : "") + "</button>";
276
+ h += '<p class="note">Card, Apple Pay or Google Pay on the shop\'s secure Square checkout. This card becomes your pickup pass.</p>';
277
+ }
220
278
  if (s.rail === "test") h += '<p class="test">Test order: no money moved.</p>';
221
- var actions = "";
222
- if (status === "awaiting_payment" && s.checkoutUrl) actions += '<button class="pay" data-open="checkoutUrl">Pay' + (s.total ? " " + esc(s.total) : "") + " by card</button>";
223
- if (live && s.passUrl) actions += '<button class="link" data-open="passUrl">Open the pass</button>';
224
- if (actions) h += '<div class="row">' + actions + "</div>";
279
+ if (live && s.passUrl) h += '<div class="actions"><button class="link" data-open="passUrl">Open the pass</button></div>';
225
280
  h += '<div class="foot">' + (s.inviteUrl ? '<button class="link" data-open="inviteUrl">Invite a friend</button>' : "<span></span>") + "<span>agentPickup</span></div></div>";
226
281
  return h;
227
282
  }
@@ -266,5 +321,5 @@ button.link{background:none;border:none;padding:0;color:var(--muted);text-decora
266
321
  </html>`;
267
322
  /** The card's page. Its version goes in the handshake, so a host's logs say which card it ran. */
268
323
  export function passCardHtml(version) {
269
- return PAGE.replace("__VERSION__", version);
324
+ return PAGE.replace("__VERSION__", version).replace("__QR_PATH__", QR_PATH_JS);
270
325
  }
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.3";
3
+ export declare const VERSION = "0.4.4";
4
4
  export interface PickupMcpOptions {
5
5
  /** The agentPickup shop this server orders from. */
6
6
  shopUrl: string;
package/dist/server.js CHANGED
@@ -7,26 +7,43 @@ import { z } from "zod";
7
7
  import { CARD_META_KEY, CARD_MIME_TYPE, passCardHtml } from "./card.js";
8
8
  import { SHARE_META_KEY, shareCardHtml } from "./share-card.js";
9
9
  import { errorOf, usd } from "./wallet.js";
10
- export const VERSION = "0.4.3";
10
+ export const VERSION = "0.4.4";
11
11
  // The pickup pass card (MCP Apps). The version is in the URI so hosts that cache cards pick up a new one.
12
12
  const CARD_URI = `ui://agentpickup/pass-${VERSION}.html`;
13
13
  const CARD_TOOL_META = { ui: { resourceUri: CARD_URI }, "ui/resourceUri": CARD_URI };
14
14
  const SHARE_URI = `ui://agentpickup/share-${VERSION}.html`;
15
15
  const SHARE_TOOL_META = { ui: { resourceUri: SHARE_URI }, "ui/resourceUri": SHARE_URI };
16
- // Shop names for the card, so polling order_status needn't read the catalog each time. Shared across the
17
- // per-request servers a hosted endpoint makes.
16
+ // Shop names and where to collect, for the card, so polling order_status needn't read the catalog each time.
17
+ // Shared across the per-request servers a hosted endpoint makes.
18
18
  const shopNames = new Map();
19
- const rememberShop = (shop, name) => {
19
+ const shopWhere = new Map();
20
+ const rememberShop = (shop, name, location) => {
20
21
  if (typeof name !== "string" || !name)
21
22
  return;
22
- if (shopNames.size > 500)
23
+ if (shopNames.size > 500) {
23
24
  shopNames.clear();
25
+ shopWhere.clear();
26
+ }
24
27
  shopNames.set(shop, name);
28
+ const where = location?.address || location?.name;
29
+ if (where)
30
+ shopWhere.set(shop, where);
25
31
  };
26
32
  /** A shop's name as the add page's ?shop= matches it: "Demo Café" -> "demo-cafe". */
27
33
  const slugOf = (name) => name.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
28
- /** The QR in ink on transparent: the card's tile behind it is white, or lime once the order is ready. */
29
- const qrSvg = (content) => QRCode.toString(content, { type: "svg", errorCorrectionLevel: "M", margin: 0, color: { dark: "#0a0b0dff", light: "#00000000" } });
34
+ /**
35
+ * A QR as a compact bit grid for the cards (a few hundred characters, where an SVG is several thousand): the cards
36
+ * draw it, and the assistant that also reads structuredContent isn't handed an image's worth of path data.
37
+ */
38
+ const qrBits = (content) => {
39
+ const { modules } = QRCode.create(content, { errorCorrectionLevel: "M" });
40
+ const n = modules.size;
41
+ const bytes = new Uint8Array(Math.ceil((n * n) / 8));
42
+ for (let i = 0; i < n * n; i++)
43
+ if (modules.data[i])
44
+ bytes[i >> 3] |= 0x80 >> (i & 7);
45
+ return { size: n, bits: Buffer.from(bytes).toString("base64") };
46
+ };
30
47
  class ToolError extends Error {
31
48
  }
32
49
  const INSTRUCTIONS = `agentPickup lets you buy things online for the user to collect in person.
@@ -48,9 +65,10 @@ Group orders (a coffee run): start_group_order gives a code to share; each perso
48
65
  pays its own share with pay_order; at closing time the shop gets one combined order and the organiser gets its QR.
49
66
  group_order_status shows who's in; close_group_order closes it early.`;
50
67
  const money = (cents, currency) => new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(cents / 100);
68
+ /** A tool's answer, and the card's data where there's a card: in structuredContent, which hosts pass to the card. */
51
69
  const reply = (summary, data, card) => ({
52
70
  content: [{ type: "text", text: `${summary}\n\n${JSON.stringify(data, null, 2)}` }],
53
- ...(card ? { _meta: { [CARD_META_KEY]: card } } : {}),
71
+ ...(card ? { structuredContent: { card }, _meta: { [CARD_META_KEY]: card } } : {}),
54
72
  });
55
73
  const failure = (message) => ({ content: [{ type: "text", text: message }], isError: true });
56
74
  /** A shop's base URL: shops can live under a path (https://host/v/cafe). A pasted API or pass link works too. */
@@ -113,7 +131,8 @@ export function createPickupMcpServer(opts) {
113
131
  async function shopName(shop) {
114
132
  if (!shopNames.has(shop)) {
115
133
  const data = await call(shop, "/api/catalog").catch(() => null);
116
- rememberShop(shop, data?.merchant?.name);
134
+ const merchant = data?.merchant;
135
+ rememberShop(shop, merchant?.name, merchant?.location);
117
136
  }
118
137
  return shopNames.get(shop);
119
138
  }
@@ -124,6 +143,12 @@ export function createPickupMcpServer(opts) {
124
143
  orderId: order.id,
125
144
  shopUrl: shop,
126
145
  shopName: shopNames.get(shop),
146
+ where: shopWhere.get(shop),
147
+ lines: order.items.map((i) => ({
148
+ qty: i.qty,
149
+ name: i.name,
150
+ ...(typeof i.unitCents === "number" ? { price: money(i.unitCents * i.qty, order.currency) } : {}),
151
+ })),
127
152
  status: order.status,
128
153
  rail: order.rail,
129
154
  items: order.items.map((i) => `${i.qty}× ${i.name}`).join(", "),
@@ -132,7 +157,7 @@ export function createPickupMcpServer(opts) {
132
157
  collectedAt: order.collectedAt ?? null,
133
158
  inviteUrl: addLinks(shop).sharePageUrl,
134
159
  ...extra,
135
- ...(pass ? { passUrl: pass.passUrl, pickupCode: pass.pickupCode, qrSvg: await qrSvg(pass.qrContent) } : {}),
160
+ ...(pass ? { passUrl: pass.passUrl, pickupCode: pass.pickupCode, qr: qrBits(pass.qrContent) } : {}),
136
161
  };
137
162
  }
138
163
  /** The add page for sharing: for one of this host's shops when given (?shop=<slug>), else everyone's. */
@@ -245,7 +270,7 @@ export function createPickupMcpServer(opts) {
245
270
  const shop = await shopFor(args.shop_url);
246
271
  const data = await call(shop, "/api/catalog");
247
272
  const merchant = data.merchant;
248
- rememberShop(shop, merchant.name);
273
+ rememberShop(shop, merchant.name, merchant.location);
249
274
  const currency = String(data.currency ?? "usd");
250
275
  const items = data.items.map((i) => ({
251
276
  id: i.id,
@@ -292,7 +317,7 @@ export function createPickupMcpServer(opts) {
292
317
  // The agent pays over x402 where the shop takes it, a test shop marks the order paid straight away, and a
293
318
  // shop that takes cards gives the user a checkout link.
294
319
  const catalog = (await call(shop, "/api/catalog"));
295
- rememberShop(shop, catalog.merchant?.name);
320
+ rememberShop(shop, catalog.merchant?.name, catalog.merchant?.location);
296
321
  const { payments } = catalog;
297
322
  const agentRail = payments.x402 ? "x402" : payments.test ? "test" : null;
298
323
  const cardRail = payments.square ? "square" : payments.card ? "card" : null;
@@ -461,7 +486,7 @@ export function createPickupMcpServer(opts) {
461
486
  const card = {
462
487
  v: 1,
463
488
  ...links,
464
- qrSvg: await QRCode.toString(links.url, { type: "svg", errorCorrectionLevel: "M", margin: 0, color: { dark: "#0a0b0dff", light: "#00000000" } }),
489
+ qr: qrBits(links.url),
465
490
  ...(name ? { shopName: name } : {}),
466
491
  };
467
492
  return {
@@ -472,6 +497,7 @@ export function createPickupMcpServer(opts) {
472
497
  `then just asks for their usual${name ? ` at ${name}` : ""}. ${links.sharePageUrl} opens the phone's share sheet and has a sticker to print.`,
473
498
  },
474
499
  ],
500
+ structuredContent: { share: card },
475
501
  _meta: { [SHARE_META_KEY]: card },
476
502
  };
477
503
  }));
@@ -1,3 +1,4 @@
1
+ import { type QrBits } from "./card.js";
1
2
  export declare const SHARE_META_KEY = "agentpickup/share";
2
3
  export interface ShareCard {
3
4
  v: 1;
@@ -5,8 +6,8 @@ export interface ShareCard {
5
6
  url: string;
6
7
  /** The add page's share mode: the share sheet and the sticker. */
7
8
  sharePageUrl: string;
8
- /** The QR as SVG, ink on transparent, for the white tile. */
9
- qrSvg: string;
9
+ /** The QR as a bit grid, for the white tile. */
10
+ qr: QrBits;
10
11
  /** A café to name, when the link is for one. */
11
12
  shopName?: string;
12
13
  }
@@ -2,141 +2,146 @@
2
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
3
  // buttons that open the add page's share mode (the phone's share sheet, and the printable sticker). Self-contained,
4
4
  // like the pass card: no scripts, fonts or images from anywhere else, and the same small MCP Apps bridge.
5
+ import { QR_PATH_JS } from "./card.js";
5
6
  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>
7
+ const PAGE = String.raw `<!doctype html>
8
+ <html lang="en">
9
+ <head>
10
+ <meta charset="utf-8">
11
+ <meta name="viewport" content="width=device-width,initial-scale=1">
12
+ <title>Share agentPickup</title>
13
+ <style>
14
+ :root{--bg:#0a0b0d;--text:#eef0f3;--muted:#b7bdc6;--dim:#8b929c;--line:#2a2f36;--lime:#b5f23d;--ink:#0a0b0d}
15
+ html,body{margin:0;padding:0;background:transparent}
16
+ body{font-family:var(--font-sans,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif);padding:2px;-webkit-font-smoothing:antialiased}
17
+ .card{box-sizing:border-box;max-width:420px;margin:0 auto;background:var(--bg);color:var(--text);border-radius:18px;padding:20px 20px 16px}
18
+ .top{display:flex;align-items:center;gap:8px;font-weight:700;font-size:15px}
19
+ .top svg{width:22px;height:22px;border-radius:5px}
20
+ .top .dim{color:var(--dim)}
21
+ h1{margin:14px 0 0;font-size:34px;line-height:.95;font-weight:850;letter-spacing:-.02em;text-transform:uppercase}
22
+ h1 em{font-style:normal;color:var(--lime)}
23
+ .sub{margin:8px 0 0;color:var(--muted);font-size:15px}
24
+ .row{display:flex;gap:16px;align-items:flex-end;margin-top:16px}
25
+ .tile{background:#fff;border-radius:14px;padding:12px;width:156px;height:156px;flex:none;box-sizing:border-box}
26
+ .tile svg{display:block;width:100%;height:100%}
27
+ ol{list-style:none;margin:0;padding:0;counter-reset:n;color:var(--muted);font-size:15px}
28
+ li{counter-increment:n;margin:0 0 8px;display:flex;align-items:center;gap:8px}
29
+ 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}
30
+ .link{margin-top:14px;display:flex;gap:8px;align-items:center}
31
+ .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}
32
+ 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}
33
+ button.lime{background:var(--lime);color:var(--ink);border-color:var(--lime)}
34
+ .actions{display:flex;gap:8px;margin-top:10px;flex-wrap:wrap}
35
+ .actions button{flex:1}
36
+ .note{margin:10px 0 0;font-size:12px;color:var(--dim)}
37
+ @media (max-width:360px){.row{flex-direction:column;align-items:flex-start}}
38
+ </style>
39
+ </head>
40
+ <body>
41
+ <div id="root"></div>
42
+ <script>
43
+ (function () {
44
+ var VERSION = "__VERSION__";
45
+ var root = document.getElementById("root");
46
+ var state = null, canOpen = false, nextId = 1, pending = {}, lastHeight = -1;
47
+ var qrPathFromBits = __QR_PATH__;
48
+ var host = window.parent;
49
+ function send(msg) { try { host.postMessage(msg, "*"); } catch (e) {} }
50
+ function request(method, params) {
51
+ var id = nextId++;
52
+ send({ jsonrpc: "2.0", id: id, method: method, params: params });
53
+ return new Promise(function (resolve, reject) {
54
+ pending[id] = { resolve: resolve, reject: reject };
55
+ setTimeout(function () { if (pending[id]) { delete pending[id]; reject(new Error("timed out")); } }, 30000);
56
+ });
57
+ }
58
+ function notify(method, params) { send({ jsonrpc: "2.0", method: method, params: params }); }
59
+ window.addEventListener("message", function (e) {
60
+ if (host !== window && e.source !== host) return;
61
+ var m = e.data;
62
+ if (!m || m.jsonrpc !== "2.0") return;
63
+ if (m.id != null && !m.method) {
64
+ var p = pending[m.id];
65
+ if (p) { delete pending[m.id]; if (m.error) p.reject(m.error); else p.resolve(m.result); }
66
+ return;
67
+ }
68
+ if (m.method === "ui/notifications/tool-result") return onResult(m.params);
69
+ if (m.method === "ui/notifications/host-context-changed") { applyContext(m.params || {}); return; }
70
+ if (m.method === "ui/resource-teardown" && m.id != null) return send({ jsonrpc: "2.0", id: m.id, result: {} });
71
+ if (m.method === "ping" && m.id != null) return send({ jsonrpc: "2.0", id: m.id, result: {} });
72
+ 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" } });
73
+ });
74
+ function applyContext(ctx) {
75
+ var vars = ctx.styles && ctx.styles.variables;
76
+ if (vars && typeof vars["--font-sans"] === "string") document.documentElement.style.setProperty("--font-sans", vars["--font-sans"]);
77
+ }
78
+ function openLink(url) {
79
+ if (!url) return;
80
+ if (canOpen) request("ui/open-link", { url: url }).catch(function () { window.open(url, "_blank", "noopener"); });
81
+ else window.open(url, "_blank", "noopener");
82
+ }
83
+ function onResult(result) {
84
+ var data = (result && result.structuredContent && result.structuredContent.share) || (result && result._meta && result._meta["agentpickup/share"]);
85
+ if (data && data.url) state = data;
86
+ render();
87
+ }
88
+ function esc(s) { return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) { return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]; }); }
89
+ function qrSvg(qr) {
90
+ if (!qr || typeof qr.bits !== "string" || !(qr.size > 0 && qr.size < 200)) return "";
91
+ try { return '<svg viewBox="0 0 ' + qr.size + " " + qr.size + '" shape-rendering="crispEdges" role="img" aria-label="QR code to add agentPickup"><path fill="#0a0b0d" d="' + qrPathFromBits(qr) + '"/></svg>'; } catch (e) { return ""; }
92
+ }
93
+ function copy(text, button) {
94
+ var done = function () { button.textContent = "Copied"; setTimeout(function () { button.textContent = "Copy"; }, 1600); };
95
+ try {
96
+ if (navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(text).then(done, function () { select(); });
97
+ } catch (e) {}
98
+ select();
99
+ }
100
+ function select() {
101
+ var code = document.getElementById("url");
102
+ if (!code || !window.getSelection) return;
103
+ var range = document.createRange();
104
+ range.selectNodeContents(code);
105
+ var sel = window.getSelection();
106
+ sel.removeAllRanges();
107
+ sel.addRange(range);
108
+ }
109
+ function render() {
110
+ if (!state) { root.innerHTML = ""; return reportSize(); }
111
+ var svg = qrSvg(state.qr);
112
+ var shown = state.url.replace(/^https?:\/\//, "");
113
+ root.innerHTML =
114
+ '<div class="card">' +
115
+ '<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>' +
116
+ "<h1>" + (state.shopName ? "Order from " + esc(state.shopName) + "<br>with your <em>AI</em>" : "Order ahead<br>from your <em>AI</em>") + "</h1>" +
117
+ '<p class="sub">Claude, ChatGPT or Grok</p>' +
118
+ '<div class="row">' + (svg ? '<div class="tile">' + svg + "</div>" : "") +
119
+ "<ol><li>Scan</li><li>Add agentPickup</li><li>Ask for your usual</li></ol></div>" +
120
+ '<div class="link"><code id="url">' + esc(shown) + '</code><button type="button" id="copy">Copy</button></div>' +
121
+ '<div class="actions"><button class="lime" type="button" id="share">Share</button><button type="button" id="sticker">Get the sticker</button></div>' +
122
+ '<p class="note">A friend scans the QR off your screen, or you send them the link.</p>' +
123
+ "</div>";
124
+ document.getElementById("copy").addEventListener("click", function (e) { copy(state.url, e.currentTarget); });
125
+ document.getElementById("share").addEventListener("click", function () { openLink(state.sharePageUrl); });
126
+ document.getElementById("sticker").addEventListener("click", function () { openLink(state.sharePageUrl + (state.sharePageUrl.indexOf("?") >= 0 ? "&" : "?") + "sticker=1"); });
127
+ reportSize();
128
+ }
129
+ function reportSize() {
130
+ var height = Math.ceil(document.body.getBoundingClientRect().height);
131
+ if (height !== lastHeight) { lastHeight = height; notify("ui/notifications/size-changed", { height: height }); }
132
+ }
133
+ if (window.ResizeObserver) new ResizeObserver(reportSize).observe(document.body);
134
+ request("ui/initialize", { appInfo: { name: "agentpickup-share", version: VERSION }, appCapabilities: {}, protocolVersion: "2026-01-26" })
135
+ .then(function (r) {
136
+ canOpen = Boolean(r && r.hostCapabilities && r.hostCapabilities.openLinks);
137
+ applyContext((r && r.hostContext) || {});
138
+ notify("ui/notifications/initialized", {});
139
+ render();
140
+ }, function () { render(); });
141
+ })();
142
+ </script>
143
+ </body>
139
144
  </html>`;
140
145
  export function shareCardHtml(version) {
141
- return PAGE.replace("__VERSION__", version);
146
+ return PAGE.replace("__VERSION__", version).replace("__QR_PATH__", QR_PATH_JS);
142
147
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentpickup/mcp",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
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",