@agentpickup/mcp 0.4.2 → 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 +23 -3
- package/dist/card.js +116 -61
- package/dist/server.d.ts +1 -1
- package/dist/server.js +48 -14
- package/dist/share-card.d.ts +3 -2
- package/dist/share-card.js +139 -134
- package/package.json +1 -1
package/dist/card.d.ts
CHANGED
|
@@ -1,27 +1,47 @@
|
|
|
1
1
|
export declare const CARD_MIME_TYPE = "text/html;profile=mcp-app";
|
|
2
|
-
/**
|
|
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
|
|
22
|
-
|
|
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.
|
|
3
|
-
//
|
|
4
|
-
// tile turns brand lime. One self-contained page: no scripts,
|
|
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
|
-
/**
|
|
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,.
|
|
20
|
-
:root[data-theme=dark]{--bg:#1d1d20;--surface:#
|
|
21
|
-
@media (prefers-color-scheme:dark){:root:not([data-theme]){--bg:#1d1d20;--surface:#
|
|
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:
|
|
25
|
-
.head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px
|
|
26
|
-
.shop{font-weight:
|
|
27
|
-
.
|
|
28
|
-
.
|
|
29
|
-
.
|
|
30
|
-
.
|
|
31
|
-
.
|
|
32
|
-
.
|
|
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:
|
|
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:
|
|
41
|
-
.facts{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:
|
|
42
|
-
.fact{background:var(--surface);border-radius:
|
|
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:
|
|
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;
|
|
47
|
-
.step{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--
|
|
48
|
-
.
|
|
49
|
-
.step.past{color:var(--ok)}.step.past
|
|
50
|
-
.step.now{color:var(--text);font-weight:
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
.
|
|
58
|
-
.foot
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
241
|
+
var unpaid = status === "awaiting_payment";
|
|
194
242
|
var live = status === "paid" || status === "ready";
|
|
195
|
-
var h = '<div class="card">';
|
|
196
|
-
h += '<
|
|
197
|
-
|
|
198
|
-
h += '
|
|
199
|
-
|
|
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>✓</b>Collected</div></div></div>
|
|
263
|
+
h += '<div class="wrap"><div class="tile plain"><div class="done"><b>✓</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
|
-
|
|
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
|
-
|
|
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
|
+
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
|
@@ -1,32 +1,49 @@
|
|
|
1
1
|
// The agentPickup MCP server: lets any MCP client (Claude, ChatGPT, Cursor, an agent framework) find pickup
|
|
2
2
|
// shops near someone, see what they sell, order, pay over x402, and hand its user the QR to collect with.
|
|
3
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
4
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
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
8
|
import { SHARE_META_KEY, shareCardHtml } from "./share-card.js";
|
|
9
9
|
import { errorOf, usd } from "./wallet.js";
|
|
10
|
-
export const VERSION = "0.4.
|
|
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.
|
|
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
|
|
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
|
-
/**
|
|
29
|
-
|
|
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
|
-
|
|
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,
|
|
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. */
|
|
@@ -169,6 +194,11 @@ export function createPickupMcpServer(opts) {
|
|
|
169
194
|
}, async () => ({
|
|
170
195
|
contents: [{ uri: CARD_URI, mimeType: CARD_MIME_TYPE, text: passCardHtml(VERSION), _meta: { ui: { prefersBorder: false } } }],
|
|
171
196
|
}));
|
|
197
|
+
// Apps that loaded an older tool list (a phone that hasn't reconnected, a ChatGPT app version fixed at review) ask
|
|
198
|
+
// for an older version's card: they get today's.
|
|
199
|
+
server.registerResource("pickup-pass-earlier", new ResourceTemplate("ui://agentpickup/pass-{version}.html", { list: undefined }), { title: "Pickup pass", mimeType: CARD_MIME_TYPE, _meta: { ui: { prefersBorder: false } } }, async (uri) => ({
|
|
200
|
+
contents: [{ uri: uri.href, mimeType: CARD_MIME_TYPE, text: passCardHtml(VERSION), _meta: { ui: { prefersBorder: false } } }],
|
|
201
|
+
}));
|
|
172
202
|
if (opts.directoryUrl) {
|
|
173
203
|
const directoryUrl = opts.directoryUrl;
|
|
174
204
|
server.registerTool("find_shops", {
|
|
@@ -240,7 +270,7 @@ export function createPickupMcpServer(opts) {
|
|
|
240
270
|
const shop = await shopFor(args.shop_url);
|
|
241
271
|
const data = await call(shop, "/api/catalog");
|
|
242
272
|
const merchant = data.merchant;
|
|
243
|
-
rememberShop(shop, merchant.name);
|
|
273
|
+
rememberShop(shop, merchant.name, merchant.location);
|
|
244
274
|
const currency = String(data.currency ?? "usd");
|
|
245
275
|
const items = data.items.map((i) => ({
|
|
246
276
|
id: i.id,
|
|
@@ -287,7 +317,7 @@ export function createPickupMcpServer(opts) {
|
|
|
287
317
|
// The agent pays over x402 where the shop takes it, a test shop marks the order paid straight away, and a
|
|
288
318
|
// shop that takes cards gives the user a checkout link.
|
|
289
319
|
const catalog = (await call(shop, "/api/catalog"));
|
|
290
|
-
rememberShop(shop, catalog.merchant?.name);
|
|
320
|
+
rememberShop(shop, catalog.merchant?.name, catalog.merchant?.location);
|
|
291
321
|
const { payments } = catalog;
|
|
292
322
|
const agentRail = payments.x402 ? "x402" : payments.test ? "test" : null;
|
|
293
323
|
const cardRail = payments.square ? "square" : payments.card ? "card" : null;
|
|
@@ -439,6 +469,9 @@ export function createPickupMcpServer(opts) {
|
|
|
439
469
|
}, async () => ({
|
|
440
470
|
contents: [{ uri: SHARE_URI, mimeType: CARD_MIME_TYPE, text: shareCardHtml(VERSION), _meta: { ui: { prefersBorder: false } } }],
|
|
441
471
|
}));
|
|
472
|
+
server.registerResource("share-agentpickup-earlier", new ResourceTemplate("ui://agentpickup/share-{version}.html", { list: undefined }), { title: "Share agentPickup", mimeType: CARD_MIME_TYPE, _meta: { ui: { prefersBorder: false } } }, async (uri) => ({
|
|
473
|
+
contents: [{ uri: uri.href, mimeType: CARD_MIME_TYPE, text: shareCardHtml(VERSION), _meta: { ui: { prefersBorder: false } } }],
|
|
474
|
+
}));
|
|
442
475
|
server.registerTool("share_agentpickup", {
|
|
443
476
|
title: "Share agentPickup",
|
|
444
477
|
description: "A QR code and link the user can share so friends can add agentPickup to Claude, ChatGPT or Grok and order too. " +
|
|
@@ -453,7 +486,7 @@ export function createPickupMcpServer(opts) {
|
|
|
453
486
|
const card = {
|
|
454
487
|
v: 1,
|
|
455
488
|
...links,
|
|
456
|
-
|
|
489
|
+
qr: qrBits(links.url),
|
|
457
490
|
...(name ? { shopName: name } : {}),
|
|
458
491
|
};
|
|
459
492
|
return {
|
|
@@ -464,6 +497,7 @@ export function createPickupMcpServer(opts) {
|
|
|
464
497
|
`then just asks for their usual${name ? ` at ${name}` : ""}. ${links.sharePageUrl} opens the phone's share sheet and has a sticker to print.`,
|
|
465
498
|
},
|
|
466
499
|
],
|
|
500
|
+
structuredContent: { share: card },
|
|
467
501
|
_meta: { [SHARE_META_KEY]: card },
|
|
468
502
|
};
|
|
469
503
|
}));
|
package/dist/share-card.d.ts
CHANGED
|
@@ -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
|
|
9
|
-
|
|
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
|
}
|
package/dist/share-card.js
CHANGED
|
@@ -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
|
|
47
|
-
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (m
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (m.method === "ui/notifications/
|
|
68
|
-
if (m.method === "ui/
|
|
69
|
-
if (m.method === "
|
|
70
|
-
if (m.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
function
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
var
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
'<div class="
|
|
114
|
-
"
|
|
115
|
-
|
|
116
|
-
'<
|
|
117
|
-
'<
|
|
118
|
-
"</div>"
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
|
|
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 { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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
|
+
"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",
|