@agentpickup/mcp 0.3.3 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -4
- package/dist/card.d.ts +27 -0
- package/dist/card.js +270 -0
- package/dist/server.d.ts +6 -1
- package/dist/server.js +185 -29
- package/dist/share-card.d.ts +13 -0
- package/dist/share-card.js +142 -0
- package/package.json +63 -62
package/README.md
CHANGED
|
@@ -10,18 +10,30 @@ QR they show at the counter to collect.
|
|
|
10
10
|
|---|---|
|
|
11
11
|
| `find_shops` | Shops near a point (`lat`/`lng`), in a suburb (`place`) or selling something (`query`), from the agentPickup directory: each one's `shop_url`, address, hours, open now, distance and matching items |
|
|
12
12
|
| `get_shop` | Items and prices, the pickup location, and how the shop takes payment |
|
|
13
|
-
| `create_pickup_order` | Creates an order. Needs the user's email, where the shop sends the receipt and pickup
|
|
13
|
+
| `create_pickup_order` | Creates an order and returns a `claim` to keep with it. Needs the user's email, where the shop sends the receipt and pickup pass. A test shop marks it paid straight away, with no money moving. Where the shop takes cards, `payment: "card"` returns a checkout link for the user to pay on instead |
|
|
14
14
|
| `pay_order` | Pays the order's x402 link from your wallet and returns the pass link. With no wallet set up, returns the price and pay link instead. For a card order, returns its checkout link |
|
|
15
|
-
| `order_status` | Waiting for payment, paid, ready, collected or expired |
|
|
16
|
-
| `get_pickup_pass` | Fetches the pass link, pickup code and QR again by signing in as the paying wallet (Sign-In-With-X) |
|
|
15
|
+
| `order_status` | Waiting for payment, paid, ready, collected or expired. With the order's `claim`, a paid order's pass comes back too |
|
|
16
|
+
| `get_pickup_pass` | Fetches the pass link, pickup code and QR again: with the order's `claim`, or by signing in as the paying wallet (Sign-In-With-X) |
|
|
17
17
|
| `start_group_order` | Starts a group order (a coffee run) with a closing time; returns the code to share and a key to close it early |
|
|
18
18
|
| `join_group_order` | Adds the user's items to a group order by its code; pay the share with `pay_order` before it closes |
|
|
19
19
|
| `group_order_status` | Who's in, what they ordered, who has paid, and whether the shop's order is ready or collected |
|
|
20
|
+
| `share_agentpickup` | A QR and link for friends to add agentPickup to their assistant (as a card where the chat shows cards), for everyone or with one café picked |
|
|
20
21
|
| `close_group_order` | Closes a group order early, as its organiser; the shop gets one combined order and the organiser its QR |
|
|
21
22
|
|
|
23
|
+
## The pickup pass card
|
|
24
|
+
|
|
25
|
+
In chat apps that draw [MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview) cards (Claude and ChatGPT
|
|
26
|
+
among them), `create_pickup_order`, `order_status` and `get_pickup_pass` show the order as a card: a Pay button while
|
|
27
|
+
it's unpaid, then the QR and pickup code, kept up to date until it's collected. When the shop marks it ready, the QR's
|
|
28
|
+
tile turns lime. The card is one self-contained page (`ui://agentpickup/pass-<version>.html`) that loads nothing from
|
|
29
|
+
anywhere else. In apps without cards, the agent gives the user the pass link and pickup code. The shop emails the pass
|
|
30
|
+
either way. The card's "Invite a friend" link, and `share_agentpickup`'s own card, point at the add page
|
|
31
|
+
(`addPageUrl`, default https://agentpickup.app/add) so friends can add it too.
|
|
32
|
+
|
|
22
33
|
## Use it
|
|
23
34
|
|
|
24
|
-
Try it against the hosted demo
|
|
35
|
+
Try it against the hosted demo shops first. The hosted server holds no wallet: its demo shops take a free test
|
|
36
|
+
payment, the Square sandbox's test card, or USDC from the user's own x402 wallet:
|
|
25
37
|
|
|
26
38
|
```json
|
|
27
39
|
{ "mcpServers": { "agentpickup": { "type": "http", "url": "https://agentpickup.app/mcp" } } }
|
package/dist/card.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
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. */
|
|
3
|
+
export declare const CARD_META_KEY = "agentpickup/pass";
|
|
4
|
+
/** What the card draws. Every field but orderId is optional: later results fill in or update what's there. */
|
|
5
|
+
export interface PassCard {
|
|
6
|
+
v: 1;
|
|
7
|
+
orderId: string;
|
|
8
|
+
shopUrl?: string;
|
|
9
|
+
shopName?: string;
|
|
10
|
+
/** Lets the card call order_status itself while the order moves along. */
|
|
11
|
+
claim?: string;
|
|
12
|
+
status?: string;
|
|
13
|
+
rail?: string;
|
|
14
|
+
items?: string;
|
|
15
|
+
total?: string;
|
|
16
|
+
collectBy?: number | null;
|
|
17
|
+
collectedAt?: number | null;
|
|
18
|
+
checkoutUrl?: string;
|
|
19
|
+
passUrl?: string;
|
|
20
|
+
pickupCode?: string | null;
|
|
21
|
+
/** The QR as SVG, ink on transparent, so the tile behind it sets the colour. */
|
|
22
|
+
qrSvg?: string;
|
|
23
|
+
/** The add page's share mode, for "Invite a friend". */
|
|
24
|
+
inviteUrl?: string;
|
|
25
|
+
}
|
|
26
|
+
/** The card's page. Its version goes in the handshake, so a host's logs say which card it ran. */
|
|
27
|
+
export declare function passCardHtml(version: string): string;
|
package/dist/card.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
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.
|
|
5
|
+
//
|
|
6
|
+
// The bridge is the MCP Apps postMessage protocol (JSON-RPC 2.0 with the host, protocol 2026-01-26), written out
|
|
7
|
+
// here rather than bundling the 300 KB SDK: ui/initialize, ui/notifications/initialized, tool-result,
|
|
8
|
+
// host-context-changed, resource-teardown, tools/call, ui/open-link and ui/notifications/size-changed.
|
|
9
|
+
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. */
|
|
11
|
+
export const CARD_META_KEY = "agentpickup/pass";
|
|
12
|
+
const PAGE = String.raw `<!doctype html>
|
|
13
|
+
<html lang="en">
|
|
14
|
+
<head>
|
|
15
|
+
<meta charset="utf-8">
|
|
16
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
17
|
+
<title>Pickup pass</title>
|
|
18
|
+
<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}}
|
|
22
|
+
html,body{margin:0;padding:0;background:transparent}
|
|
23
|
+
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}
|
|
33
|
+
.pour{position:absolute;left:0;right:0;bottom:0;height:0;background:var(--lime);transition:height 1s cubic-bezier(.5,0,.2,1)}
|
|
34
|
+
.wrap.ready .pour{height:100%}
|
|
35
|
+
.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}
|
|
37
|
+
@keyframes ring{from{transform:scale(1);opacity:.9}to{transform:scale(1.08);opacity:0}}
|
|
38
|
+
.tile.plain{background:var(--surface)}
|
|
39
|
+
.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}
|
|
43
|
+
.fact span{display:block;font-size:12px;color:var(--muted)}
|
|
44
|
+
.fact b{display:block;font-size:20px;font-weight:600;overflow-wrap:anywhere}
|
|
45
|
+
.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)}
|
|
51
|
+
.msg{font-size:13px;color:var(--muted);margin:10px 0 0;line-height:1.45}
|
|
52
|
+
.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}
|
|
59
|
+
@media (prefers-reduced-motion:reduce){.pour{transition:none}.wrap.ready::after{animation:none;opacity:1}}
|
|
60
|
+
</style>
|
|
61
|
+
</head>
|
|
62
|
+
<body>
|
|
63
|
+
<div id="root"></div>
|
|
64
|
+
<script>
|
|
65
|
+
(function () {
|
|
66
|
+
var VERSION = "__VERSION__";
|
|
67
|
+
var LABELS = { awaiting_payment: "Waiting for payment", paid: "Paid", ready: "Ready", collected: "Collected", expired: "Expired", cancelled: "Cancelled" };
|
|
68
|
+
var FINAL = { collected: 1, expired: 1, cancelled: 1 };
|
|
69
|
+
var root = document.getElementById("root");
|
|
70
|
+
var state = null, canCall = false, canOpen = false, locale, timeZone;
|
|
71
|
+
var nextId = 1, pending = {}, timer = null, started = Date.now(), lastHtml = "", poured = false, lastHeight = -1, stopped = false;
|
|
72
|
+
|
|
73
|
+
// ---------- the bridge ----------
|
|
74
|
+
var host = window.parent;
|
|
75
|
+
function send(msg) { try { host.postMessage(msg, "*"); } catch (e) {} }
|
|
76
|
+
function request(method, params) {
|
|
77
|
+
var id = nextId++;
|
|
78
|
+
send({ jsonrpc: "2.0", id: id, method: method, params: params });
|
|
79
|
+
return new Promise(function (resolve, reject) {
|
|
80
|
+
pending[id] = { resolve: resolve, reject: reject };
|
|
81
|
+
setTimeout(function () { if (pending[id]) { delete pending[id]; reject(new Error("timed out")); } }, 30000);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function notify(method, params) { send({ jsonrpc: "2.0", method: method, params: params }); }
|
|
85
|
+
window.addEventListener("message", function (e) {
|
|
86
|
+
if (host !== window && e.source !== host) return;
|
|
87
|
+
var m = e.data;
|
|
88
|
+
if (!m || m.jsonrpc !== "2.0") return;
|
|
89
|
+
if (m.id != null && !m.method) {
|
|
90
|
+
var p = pending[m.id];
|
|
91
|
+
if (p) { delete pending[m.id]; if (m.error) p.reject(m.error); else p.resolve(m.result); }
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (m.method === "ui/notifications/tool-result") return onResult(m.params);
|
|
95
|
+
if (m.method === "ui/notifications/host-context-changed") { applyContext(m.params || {}); return render(); }
|
|
96
|
+
if (m.method === "ui/resource-teardown") { stop(); if (m.id != null) send({ jsonrpc: "2.0", id: m.id, result: {} }); return; }
|
|
97
|
+
if (m.method === "ping" && m.id != null) return send({ jsonrpc: "2.0", id: m.id, result: {} });
|
|
98
|
+
if (m.id != null && m.method && m.method.indexOf("ui/notifications/") !== 0) {
|
|
99
|
+
send({ jsonrpc: "2.0", id: m.id, error: { code: -32601, message: "Method not found" } });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
function applyContext(ctx) {
|
|
103
|
+
if (ctx.theme === "light" || ctx.theme === "dark") document.documentElement.setAttribute("data-theme", ctx.theme);
|
|
104
|
+
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]);
|
|
106
|
+
if (ctx.locale) locale = ctx.locale;
|
|
107
|
+
if (ctx.timeZone) timeZone = ctx.timeZone;
|
|
108
|
+
}
|
|
109
|
+
function openLink(url) {
|
|
110
|
+
if (!url) return;
|
|
111
|
+
if (canOpen) request("ui/open-link", { url: url }).catch(function () { window.open(url, "_blank", "noopener"); });
|
|
112
|
+
else window.open(url, "_blank", "noopener");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------- results ----------
|
|
116
|
+
function cardOf(result) {
|
|
117
|
+
if (!result) return null;
|
|
118
|
+
var meta = result._meta && result._meta["agentpickup/pass"];
|
|
119
|
+
if (meta && meta.orderId) return meta;
|
|
120
|
+
// A host that drops _meta: the JSON after the tool's summary line still has the basics.
|
|
121
|
+
try {
|
|
122
|
+
var text = result.content && result.content[0] && result.content[0].text;
|
|
123
|
+
var data = JSON.parse(text.slice(text.indexOf("\n\n") + 2));
|
|
124
|
+
var id = data.orderId || data.id || (data.order && data.order.id);
|
|
125
|
+
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 };
|
|
127
|
+
} catch (e) { return null; }
|
|
128
|
+
}
|
|
129
|
+
function onResult(result) {
|
|
130
|
+
var card = cardOf(result);
|
|
131
|
+
if (card) {
|
|
132
|
+
if (state && state.orderId !== card.orderId) state = null;
|
|
133
|
+
var next = state ? Object.assign({}, state) : {};
|
|
134
|
+
for (var k in card) if (card[k] !== undefined && card[k] !== null && card[k] !== "") next[k] = card[k];
|
|
135
|
+
if (card.status && card.status !== "awaiting_payment") delete next.checkoutUrl;
|
|
136
|
+
state = next;
|
|
137
|
+
}
|
|
138
|
+
render();
|
|
139
|
+
schedule();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------- keeping up to date ----------
|
|
143
|
+
function stop() { stopped = true; clearTimeout(timer); }
|
|
144
|
+
function schedule() {
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
if (stopped || !state || !state.claim || !canCall || FINAL[state.status]) return;
|
|
147
|
+
if (Date.now() - started > 4 * 3600 * 1000) return;
|
|
148
|
+
var wait = document.hidden ? 60000 : state.status === "awaiting_payment" ? 5000 : 15000;
|
|
149
|
+
timer = setTimeout(poll, wait);
|
|
150
|
+
}
|
|
151
|
+
function poll() {
|
|
152
|
+
var args = { order_id: state.orderId, claim: state.claim };
|
|
153
|
+
if (state.shopUrl) args.shop_url = state.shopUrl;
|
|
154
|
+
request("tools/call", { name: "order_status", arguments: args }).then(function (r) {
|
|
155
|
+
if (r && !r.isError) onResult(r); else schedule();
|
|
156
|
+
}, schedule);
|
|
157
|
+
}
|
|
158
|
+
document.addEventListener("visibilitychange", function () { if (!document.hidden && state) { clearTimeout(timer); poll(); } });
|
|
159
|
+
|
|
160
|
+
// ---------- drawing ----------
|
|
161
|
+
function esc(s) { return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]; }); }
|
|
162
|
+
function time(ms) {
|
|
163
|
+
var d = new Date(ms), o = { hour: "numeric", minute: "2-digit" };
|
|
164
|
+
if (timeZone) o.timeZone = timeZone;
|
|
165
|
+
try {
|
|
166
|
+
var s = d.toLocaleTimeString(locale, o);
|
|
167
|
+
if (Math.abs(ms - Date.now()) > 18 * 3600 * 1000) {
|
|
168
|
+
var od = { weekday: "short" };
|
|
169
|
+
if (timeZone) od.timeZone = timeZone;
|
|
170
|
+
s = d.toLocaleDateString(locale, od) + " " + s;
|
|
171
|
+
}
|
|
172
|
+
return s;
|
|
173
|
+
} catch (e) { return d.toLocaleTimeString(); }
|
|
174
|
+
}
|
|
175
|
+
function safeSvg(svg) { return typeof svg === "string" && /^<svg[\s>]/.test(svg) && !/<script|<foreignObject|\son[a-z]+=/i.test(svg) ? svg : ""; }
|
|
176
|
+
function message(s) {
|
|
177
|
+
var live = canCall && s.claim;
|
|
178
|
+
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." : "");
|
|
183
|
+
case "paid": return "Paid. " + (live ? "This turns green when it's ready. " : "") + "The pass is in your email too.";
|
|
184
|
+
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.";
|
|
186
|
+
case "expired": return "This order expired before anyone collected it.";
|
|
187
|
+
case "cancelled": return "This order was cancelled.";
|
|
188
|
+
default: return "";
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function view(s) {
|
|
192
|
+
var status = s.status || "awaiting_payment";
|
|
193
|
+
var label = LABELS[status] || status;
|
|
194
|
+
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);
|
|
200
|
+
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>";
|
|
203
|
+
} else if (status === "collected") {
|
|
204
|
+
h += '<div class="wrap"><div class="tile plain"><div class="done"><b>✓</b>Collected</div></div></div>';
|
|
205
|
+
}
|
|
206
|
+
if (live && (s.pickupCode || s.collectBy)) {
|
|
207
|
+
h += '<div class="facts">';
|
|
208
|
+
if (s.pickupCode) h += '<div class="fact"><span>Pickup code</span><b class="code">' + esc(s.pickupCode) + "</b></div>";
|
|
209
|
+
if (s.collectBy) h += '<div class="fact"><span>Collect by</span><b>' + esc(time(s.collectBy)) + "</b></div>";
|
|
210
|
+
h += "</div>";
|
|
211
|
+
}
|
|
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
|
+
}
|
|
218
|
+
var msg = message(s);
|
|
219
|
+
if (msg) h += '<p class="msg">' + esc(msg) + "</p>";
|
|
220
|
+
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>";
|
|
225
|
+
h += '<div class="foot">' + (s.inviteUrl ? '<button class="link" data-open="inviteUrl">Invite a friend</button>' : "<span></span>") + "<span>agentPickup</span></div></div>";
|
|
226
|
+
return h;
|
|
227
|
+
}
|
|
228
|
+
function render() {
|
|
229
|
+
var html = state ? view(state) : "";
|
|
230
|
+
if (html !== lastHtml) {
|
|
231
|
+
root.innerHTML = html;
|
|
232
|
+
lastHtml = html;
|
|
233
|
+
var buttons = root.querySelectorAll("[data-open]");
|
|
234
|
+
for (var i = 0; i < buttons.length; i++) buttons[i].addEventListener("click", function (e) { openLink(state[e.currentTarget.getAttribute("data-open")]); });
|
|
235
|
+
}
|
|
236
|
+
// The lime pours in once, the first time the card sees the order ready.
|
|
237
|
+
var wrap = document.getElementById("wrap");
|
|
238
|
+
if (wrap && state && state.status === "ready" && !poured) {
|
|
239
|
+
void wrap.offsetWidth;
|
|
240
|
+
wrap.classList.add("ready");
|
|
241
|
+
poured = true;
|
|
242
|
+
lastHtml = view(state);
|
|
243
|
+
}
|
|
244
|
+
reportSize();
|
|
245
|
+
}
|
|
246
|
+
function reportSize() {
|
|
247
|
+
var height = Math.ceil(document.body.getBoundingClientRect().height);
|
|
248
|
+
if (height !== lastHeight) { lastHeight = height; notify("ui/notifications/size-changed", { height: height }); }
|
|
249
|
+
}
|
|
250
|
+
if (window.ResizeObserver) new ResizeObserver(reportSize).observe(document.body);
|
|
251
|
+
|
|
252
|
+
// ---------- start ----------
|
|
253
|
+
request("ui/initialize", { appInfo: { name: "agentpickup-pass", version: VERSION }, appCapabilities: {}, protocolVersion: "2026-01-26" })
|
|
254
|
+
.then(function (r) {
|
|
255
|
+
var caps = (r && r.hostCapabilities) || {};
|
|
256
|
+
canCall = Boolean(caps.serverTools);
|
|
257
|
+
canOpen = Boolean(caps.openLinks);
|
|
258
|
+
applyContext((r && r.hostContext) || {});
|
|
259
|
+
notify("ui/notifications/initialized", {});
|
|
260
|
+
render();
|
|
261
|
+
schedule();
|
|
262
|
+
}, function () { render(); });
|
|
263
|
+
})();
|
|
264
|
+
</script>
|
|
265
|
+
</body>
|
|
266
|
+
</html>`;
|
|
267
|
+
/** The card's page. Its version goes in the handshake, so a host's logs say which card it ran. */
|
|
268
|
+
export function passCardHtml(version) {
|
|
269
|
+
return PAGE.replace("__VERSION__", version);
|
|
270
|
+
}
|
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.
|
|
3
|
+
export declare const VERSION = "0.4.1";
|
|
4
4
|
export interface PickupMcpOptions {
|
|
5
5
|
/** The agentPickup shop this server orders from. */
|
|
6
6
|
shopUrl: string;
|
|
@@ -22,6 +22,11 @@ export interface PickupMcpOptions {
|
|
|
22
22
|
payer?: Payer;
|
|
23
23
|
/** Notes added to the instructions agents read on connecting, e.g. which of this host's shops take what. */
|
|
24
24
|
instructions?: string;
|
|
25
|
+
/**
|
|
26
|
+
* The page where people add agentPickup to their assistant, which share_agentpickup and the pass card's
|
|
27
|
+
* "Invite a friend" link point to. Default https://agentpickup.app/add.
|
|
28
|
+
*/
|
|
29
|
+
addPageUrl?: string;
|
|
25
30
|
}
|
|
26
31
|
export declare function createPickupMcpServer(opts: PickupMcpOptions): McpServer;
|
|
27
32
|
/** A Streamable HTTP endpoint (stateless, JSON responses) for hosting the server, e.g. at /mcp. */
|
package/dist/server.js
CHANGED
|
@@ -2,27 +2,54 @@
|
|
|
2
2
|
// shops near someone, see what they sell, order, pay over x402, and hand its user the QR to collect with.
|
|
3
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
4
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
5
|
+
import QRCode from "qrcode";
|
|
5
6
|
import { z } from "zod";
|
|
7
|
+
import { CARD_META_KEY, CARD_MIME_TYPE, passCardHtml } from "./card.js";
|
|
8
|
+
import { SHARE_META_KEY, shareCardHtml } from "./share-card.js";
|
|
6
9
|
import { errorOf, usd } from "./wallet.js";
|
|
7
|
-
export const VERSION = "0.
|
|
10
|
+
export const VERSION = "0.4.1";
|
|
11
|
+
// The pickup pass card (MCP Apps). The version is in the URI so hosts that cache cards pick up a new one.
|
|
12
|
+
const CARD_URI = `ui://agentpickup/pass-${VERSION}.html`;
|
|
13
|
+
const CARD_TOOL_META = { ui: { resourceUri: CARD_URI }, "ui/resourceUri": CARD_URI };
|
|
14
|
+
const SHARE_URI = `ui://agentpickup/share-${VERSION}.html`;
|
|
15
|
+
const SHARE_TOOL_META = { ui: { resourceUri: SHARE_URI }, "ui/resourceUri": SHARE_URI };
|
|
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.
|
|
18
|
+
const shopNames = new Map();
|
|
19
|
+
const rememberShop = (shop, name) => {
|
|
20
|
+
if (typeof name !== "string" || !name)
|
|
21
|
+
return;
|
|
22
|
+
if (shopNames.size > 500)
|
|
23
|
+
shopNames.clear();
|
|
24
|
+
shopNames.set(shop, name);
|
|
25
|
+
};
|
|
26
|
+
/** A shop's name as the add page's ?shop= matches it: "Demo Café" -> "demo-cafe". */
|
|
27
|
+
const slugOf = (name) => name.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
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" } });
|
|
8
30
|
class ToolError extends Error {
|
|
9
31
|
}
|
|
10
32
|
const INSTRUCTIONS = `agentPickup lets you buy things online for the user to collect in person.
|
|
11
33
|
0. find_shops (when available): shops near the user that sell what they want. Pass a shop's shop_url to the other tools.
|
|
12
34
|
1. get_shop: see what the shop sells, its pickup location and how it takes payment.
|
|
13
|
-
2. create_pickup_order: needs the user's email
|
|
35
|
+
2. create_pickup_order: needs the user's email; the shop emails the pickup pass there too. It returns a claim:
|
|
36
|
+
keep it with the order id.
|
|
14
37
|
3. pay_order: pays in USDC over x402 from the wallet set up here; with none, it hands back the pay link for the user's
|
|
15
38
|
own x402 wallet. Where the shop takes cards, payment: "card" on
|
|
16
39
|
create_pickup_order gives a checkout link for the user to pay on instead. Where it takes test payments (get_shop says
|
|
17
40
|
so), payment: "test" runs the whole flow with no money moving. Confirm the items and total with the user first.
|
|
18
|
-
4.
|
|
19
|
-
|
|
41
|
+
4. The pass: where the chat shows cards, the order appears as a card that becomes the pickup pass (QR and code)
|
|
42
|
+
once it's paid and keeps itself up to date until it's collected. Otherwise give the user the pass link and pickup
|
|
43
|
+
code. They show the QR at the counter, or say the code, to collect.
|
|
44
|
+
get_pickup_pass (with the claim) fetches the pass again; order_status (with the claim) says whether it's ready.
|
|
45
|
+
share_agentpickup: when the user asks how to share agentPickup or get friends using it, shows a QR and link.
|
|
20
46
|
Group orders (a coffee run): start_group_order gives a code to share; each person's agent calls join_group_order and
|
|
21
47
|
pays its own share with pay_order; at closing time the shop gets one combined order and the organiser gets its QR.
|
|
22
48
|
group_order_status shows who's in; close_group_order closes it early.`;
|
|
23
49
|
const money = (cents, currency) => new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(cents / 100);
|
|
24
|
-
const reply = (summary, data) => ({
|
|
50
|
+
const reply = (summary, data, card) => ({
|
|
25
51
|
content: [{ type: "text", text: `${summary}\n\n${JSON.stringify(data, null, 2)}` }],
|
|
52
|
+
...(card ? { _meta: { [CARD_META_KEY]: card } } : {}),
|
|
26
53
|
});
|
|
27
54
|
const failure = (message) => ({ content: [{ type: "text", text: message }], isError: true });
|
|
28
55
|
/** A shop's base URL: shops can live under a path (https://host/v/cafe). A pasted API or pass link works too. */
|
|
@@ -73,6 +100,56 @@ export function createPickupMcpServer(opts) {
|
|
|
73
100
|
return failure(`Couldn't reach the shop: ${err instanceof Error ? err.message : String(err)}`);
|
|
74
101
|
}
|
|
75
102
|
};
|
|
103
|
+
/** The pass, for whoever made the order. null while it isn't paid (or is a share of a group order). */
|
|
104
|
+
async function passByClaim(shop, orderId, claim) {
|
|
105
|
+
const res = await doFetch(new Request(`${shop}/api/orders/${encodeURIComponent(orderId)}/pass`, { headers: { "X-Pickup-Claim": claim } }));
|
|
106
|
+
if (res.status === 409)
|
|
107
|
+
return null;
|
|
108
|
+
if (!res.ok)
|
|
109
|
+
throw new ToolError(await errorOf(res, `The shop answered ${res.status}.`));
|
|
110
|
+
return (await res.json());
|
|
111
|
+
}
|
|
112
|
+
async function shopName(shop) {
|
|
113
|
+
if (!shopNames.has(shop)) {
|
|
114
|
+
const data = await call(shop, "/api/catalog").catch(() => null);
|
|
115
|
+
rememberShop(shop, data?.merchant?.name);
|
|
116
|
+
}
|
|
117
|
+
return shopNames.get(shop);
|
|
118
|
+
}
|
|
119
|
+
/** What the pass card draws for an order, with its QR once there's a pass. */
|
|
120
|
+
async function cardFor(shop, order, extra = {}, pass) {
|
|
121
|
+
return {
|
|
122
|
+
v: 1,
|
|
123
|
+
orderId: order.id,
|
|
124
|
+
shopUrl: shop,
|
|
125
|
+
shopName: shopNames.get(shop),
|
|
126
|
+
status: order.status,
|
|
127
|
+
rail: order.rail,
|
|
128
|
+
items: order.items.map((i) => `${i.qty}× ${i.name}`).join(", "),
|
|
129
|
+
total: money(order.totalCents, order.currency),
|
|
130
|
+
collectBy: order.collectBy,
|
|
131
|
+
collectedAt: order.collectedAt ?? null,
|
|
132
|
+
inviteUrl: addLinks(shop).sharePageUrl,
|
|
133
|
+
...extra,
|
|
134
|
+
...(pass ? { passUrl: pass.passUrl, pickupCode: pass.pickupCode, qrSvg: await qrSvg(pass.qrContent) } : {}),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** The add page for sharing: for one of this host's shops when given (?shop=<slug>), else everyone's. */
|
|
138
|
+
const addPage = (opts.addPageUrl ?? "https://agentpickup.app/add").replace(/\/$/, "");
|
|
139
|
+
function addLinks(shop) {
|
|
140
|
+
let slug = "";
|
|
141
|
+
if (shop) {
|
|
142
|
+
const u = new URL(shop);
|
|
143
|
+
slug = u.pathname.match(/\/v\/([^/]+)/)?.[1] ?? (u.pathname === "/" || u.pathname === "" ? slugOf(shopNames.get(shop) ?? "") : "");
|
|
144
|
+
}
|
|
145
|
+
const q = slug ? `shop=${encodeURIComponent(slug)}` : "";
|
|
146
|
+
return { url: `${addPage}${q ? `?${q}` : ""}`, sharePageUrl: `${addPage}?share=1${q ? `&${q}` : ""}` };
|
|
147
|
+
}
|
|
148
|
+
const claimArg = z
|
|
149
|
+
.string()
|
|
150
|
+
.max(200)
|
|
151
|
+
.optional()
|
|
152
|
+
.describe("The claim create_pickup_order returned with the order. With it, a paid order's pass comes back too.");
|
|
76
153
|
const shopArg = opts.allowOtherShops || opts.directoryUrl
|
|
77
154
|
? { shop_url: z.string().optional().describe(`An agentPickup shop's URL, as find_shops returns it. Defaults to ${home}.`) }
|
|
78
155
|
: {};
|
|
@@ -83,6 +160,14 @@ export function createPickupMcpServer(opts) {
|
|
|
83
160
|
websiteUrl: "https://agentpickup.app",
|
|
84
161
|
icons: [{ src: "https://agentpickup.app/logo.png", mimeType: "image/png", sizes: ["1024x1024"] }],
|
|
85
162
|
}, { instructions: opts.instructions ? `${INSTRUCTIONS}\n${opts.instructions}` : INSTRUCTIONS });
|
|
163
|
+
server.registerResource("pickup-pass", CARD_URI, {
|
|
164
|
+
title: "Pickup pass",
|
|
165
|
+
description: "The order as a card: a Pay button while it's unpaid, then the QR and pickup code, kept up to date until it's collected.",
|
|
166
|
+
mimeType: CARD_MIME_TYPE,
|
|
167
|
+
_meta: { ui: { prefersBorder: false } },
|
|
168
|
+
}, async () => ({
|
|
169
|
+
contents: [{ uri: CARD_URI, mimeType: CARD_MIME_TYPE, text: passCardHtml(VERSION), _meta: { ui: { prefersBorder: false } } }],
|
|
170
|
+
}));
|
|
86
171
|
if (opts.directoryUrl) {
|
|
87
172
|
const directoryUrl = opts.directoryUrl;
|
|
88
173
|
server.registerTool("find_shops", {
|
|
@@ -154,6 +239,7 @@ export function createPickupMcpServer(opts) {
|
|
|
154
239
|
const shop = await shopFor(args.shop_url);
|
|
155
240
|
const data = await call(shop, "/api/catalog");
|
|
156
241
|
const merchant = data.merchant;
|
|
242
|
+
rememberShop(shop, merchant.name);
|
|
157
243
|
const currency = String(data.currency ?? "usd");
|
|
158
244
|
const items = data.items.map((i) => ({
|
|
159
245
|
id: i.id,
|
|
@@ -177,14 +263,14 @@ export function createPickupMcpServer(opts) {
|
|
|
177
263
|
}));
|
|
178
264
|
server.registerTool("create_pickup_order", {
|
|
179
265
|
title: "Order for pickup",
|
|
180
|
-
description: "Create an order to collect in person. Returns the order id and
|
|
181
|
-
"The shop emails the pickup
|
|
182
|
-
"returns a checkout link for the user to pay on instead.",
|
|
266
|
+
description: "Create an order to collect in person. Returns the order id, total and a claim (keep it); pay it with pay_order. " +
|
|
267
|
+
"The shop emails the pickup pass to `email` once it's paid, and where the chat shows cards the order shows as one " +
|
|
268
|
+
"that becomes the pass. Where the shop takes cards, `payment: \"card\"` returns a checkout link for the user to pay on instead.",
|
|
183
269
|
inputSchema: {
|
|
184
270
|
items: z
|
|
185
271
|
.array(z.object({ id: z.string().describe("Item id from get_shop"), qty: z.number().int().min(1).max(20) }))
|
|
186
272
|
.min(1),
|
|
187
|
-
email: z.string().email().describe("The user's email. The pickup
|
|
273
|
+
email: z.string().email().describe("The user's email. The pickup pass goes here too."),
|
|
188
274
|
name: z.string().max(100).optional().describe("Name for the order, shown to staff"),
|
|
189
275
|
payment: z
|
|
190
276
|
.enum(["agent", "card", "test"])
|
|
@@ -194,11 +280,14 @@ export function createPickupMcpServer(opts) {
|
|
|
194
280
|
...shopArg,
|
|
195
281
|
},
|
|
196
282
|
annotations: { title: "Order for pickup", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
283
|
+
_meta: CARD_TOOL_META,
|
|
197
284
|
}, guarded(async (args) => {
|
|
198
285
|
const shop = await shopFor(args.shop_url);
|
|
199
286
|
// The agent pays over x402 where the shop takes it, a test shop marks the order paid straight away, and a
|
|
200
287
|
// shop that takes cards gives the user a checkout link.
|
|
201
|
-
const
|
|
288
|
+
const catalog = (await call(shop, "/api/catalog"));
|
|
289
|
+
rememberShop(shop, catalog.merchant?.name);
|
|
290
|
+
const { payments } = catalog;
|
|
202
291
|
const agentRail = payments.x402 ? "x402" : payments.test ? "test" : null;
|
|
203
292
|
const cardRail = payments.square ? "square" : payments.card ? "card" : null;
|
|
204
293
|
const rail = args.payment === "test" ? (payments.test ? "test" : null)
|
|
@@ -217,16 +306,31 @@ export function createPickupMcpServer(opts) {
|
|
|
217
306
|
});
|
|
218
307
|
const order = data.order;
|
|
219
308
|
const next = data.next;
|
|
309
|
+
// Older shops don't hand out claims; everything else still works.
|
|
310
|
+
const claim = typeof data.claim === "string" ? data.claim : undefined;
|
|
311
|
+
const what = `${order.items.map((i) => `${i.qty}× ${i.name}`).join(", ")}, ${money(order.totalCents, order.currency)}`;
|
|
312
|
+
const keep = claim ? " Keep the claim: get_pickup_pass and order_status take it." : "";
|
|
220
313
|
if (rail === "test") {
|
|
221
|
-
|
|
222
|
-
|
|
314
|
+
const pass = claim ? await passByClaim(shop, order.id, claim).catch(() => null) : null;
|
|
315
|
+
return reply(`Test order ${order.id}: ${what}, marked paid with no money moving. The pickup pass is at ${next.passUrl}` +
|
|
316
|
+
(pass ? ` (pickup code ${pass.pickupCode})` : "") +
|
|
317
|
+
`, and the shop emails it to ${args.email}.${keep}`, {
|
|
318
|
+
shop,
|
|
319
|
+
orderId: order.id,
|
|
320
|
+
claim,
|
|
321
|
+
status: order.status,
|
|
322
|
+
test: true,
|
|
323
|
+
total: money(order.totalCents, order.currency),
|
|
324
|
+
items: order.items,
|
|
325
|
+
passUrl: next.passUrl,
|
|
326
|
+
pickupCode: pass?.pickupCode,
|
|
327
|
+
}, await cardFor(shop, order, { claim, rail }, pass));
|
|
223
328
|
}
|
|
224
329
|
if (rail === "square" || rail === "card") {
|
|
225
|
-
return reply(`Order ${order.id}: ${
|
|
226
|
-
|
|
330
|
+
return reply(`Order ${order.id}: ${what}. Give the user this link to pay by card: ${next.url}. Once they've paid, the shop emails ` +
|
|
331
|
+
`${args.email} the receipt and pickup pass.${keep}`, { shop, orderId: order.id, claim, total: money(order.totalCents, order.currency), items: order.items, payBy: "card", checkoutUrl: next.url, collectBy: order.collectBy }, await cardFor(shop, order, { claim, rail, checkoutUrl: next.url }));
|
|
227
332
|
}
|
|
228
|
-
return reply(`Order ${order.id}: ${
|
|
229
|
-
`Confirm with the user, then call pay_order.`, { shop, orderId: order.id, total: money(order.totalCents, order.currency), items: order.items, payUrl: next.url, collectBy: order.collectBy });
|
|
333
|
+
return reply(`Order ${order.id}: ${what}. Confirm with the user, then call pay_order.${keep}`, { shop, orderId: order.id, claim, total: money(order.totalCents, order.currency), items: order.items, payUrl: next.url, collectBy: order.collectBy }, await cardFor(shop, order, { claim, rail }));
|
|
230
334
|
}));
|
|
231
335
|
server.registerTool("pay_order", {
|
|
232
336
|
title: "Pay for an order",
|
|
@@ -279,12 +383,14 @@ export function createPickupMcpServer(opts) {
|
|
|
279
383
|
}));
|
|
280
384
|
server.registerTool("order_status", {
|
|
281
385
|
title: "Check an order",
|
|
282
|
-
description: "Whether an order is waiting for payment, paid, ready to collect, collected or expired."
|
|
283
|
-
|
|
386
|
+
description: "Whether an order is waiting for payment, paid, ready to collect, collected or expired. With the order's claim, " +
|
|
387
|
+
"a paid order's pass comes back too, and the chat's card for it updates.",
|
|
388
|
+
inputSchema: { order_id: z.string(), claim: claimArg, ...shopArg },
|
|
284
389
|
annotations: { title: "Check an order", readOnlyHint: true, openWorldHint: true },
|
|
390
|
+
_meta: CARD_TOOL_META,
|
|
285
391
|
}, guarded(async (args) => {
|
|
286
392
|
const shop = await shopFor(args.shop_url);
|
|
287
|
-
const { order } = (await call(shop, `/api/orders/${encodeURIComponent(args.order_id)}`));
|
|
393
|
+
const { order, checkoutUrl } = (await call(shop, `/api/orders/${encodeURIComponent(args.order_id)}`));
|
|
288
394
|
const words = {
|
|
289
395
|
awaiting_payment: "is waiting for payment",
|
|
290
396
|
paid: "is paid and being prepared",
|
|
@@ -293,22 +399,72 @@ export function createPickupMcpServer(opts) {
|
|
|
293
399
|
expired: "expired before anyone collected it",
|
|
294
400
|
cancelled: "was cancelled",
|
|
295
401
|
};
|
|
296
|
-
|
|
402
|
+
const pass = args.claim && (order.status === "paid" || order.status === "ready") ? await passByClaim(shop, order.id, args.claim).catch(() => null) : null;
|
|
403
|
+
return reply(`Order ${order.id} ${words[order.status] ?? `is ${order.status}`}.` + (pass ? ` Pickup code ${pass.pickupCode}, pass at ${pass.passUrl}.` : ""), pass ? { ...order, passUrl: pass.passUrl, pickupCode: pass.pickupCode } : order, await cardFor(shop, order, { claim: args.claim, checkoutUrl }, pass));
|
|
297
404
|
}));
|
|
298
405
|
server.registerTool("get_pickup_pass", {
|
|
299
406
|
title: "Get the pickup pass",
|
|
300
|
-
description: "The pass link, pickup code and QR
|
|
301
|
-
|
|
407
|
+
description: "The pass link, pickup code and QR for a paid order: with the claim create_pickup_order returned" +
|
|
408
|
+
(opts.payer ? ", or by signing in with the wallet that paid (Sign-In-With-X)." : "."),
|
|
409
|
+
inputSchema: { order_id: z.string(), claim: claimArg, ...shopArg },
|
|
302
410
|
annotations: { title: "Get the pickup pass", readOnlyHint: true, openWorldHint: true },
|
|
411
|
+
_meta: CARD_TOOL_META,
|
|
303
412
|
}, guarded(async (args) => {
|
|
304
|
-
if (!opts.payer)
|
|
305
|
-
throw new ToolError("No wallet is set up here. The pass was emailed to the buyer when the order was paid.");
|
|
306
413
|
const shop = await shopFor(args.shop_url);
|
|
307
|
-
|
|
308
|
-
if (
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
414
|
+
let pass;
|
|
415
|
+
if (args.claim) {
|
|
416
|
+
pass = await passByClaim(shop, args.order_id, args.claim);
|
|
417
|
+
if (!pass)
|
|
418
|
+
throw new ToolError("This order isn't paid yet, so there's no pass. order_status says where it's at.");
|
|
419
|
+
}
|
|
420
|
+
else if (opts.payer) {
|
|
421
|
+
const res = await opts.payer.signIn(`${shop}/api/orders/${encodeURIComponent(args.order_id)}/pass`, doFetch);
|
|
422
|
+
if (!res.ok)
|
|
423
|
+
throw new ToolError(await errorOf(res, `The shop answered ${res.status}.`));
|
|
424
|
+
pass = (await res.json());
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
throw new ToolError("Send the claim create_pickup_order returned with the order. The pass is also in the email the shop sent when it was paid.");
|
|
428
|
+
}
|
|
429
|
+
await shopName(shop);
|
|
430
|
+
return reply(`Pickup pass: ${pass.passUrl}, pickup code ${pass.pickupCode}. The user shows the QR at the counter, or says the code.`, { passUrl: pass.passUrl, pickupCode: pass.pickupCode, qrContent: pass.qrContent, status: pass.order?.status }, pass.order ? await cardFor(shop, pass.order, { claim: args.claim }, pass) : undefined);
|
|
431
|
+
}));
|
|
432
|
+
// ---------- sharing ----------
|
|
433
|
+
server.registerResource("share-agentpickup", SHARE_URI, {
|
|
434
|
+
title: "Share agentPickup",
|
|
435
|
+
description: "A QR and link for friends to add agentPickup to their assistant.",
|
|
436
|
+
mimeType: CARD_MIME_TYPE,
|
|
437
|
+
_meta: { ui: { prefersBorder: false } },
|
|
438
|
+
}, async () => ({
|
|
439
|
+
contents: [{ uri: SHARE_URI, mimeType: CARD_MIME_TYPE, text: shareCardHtml(VERSION), _meta: { ui: { prefersBorder: false } } }],
|
|
440
|
+
}));
|
|
441
|
+
server.registerTool("share_agentpickup", {
|
|
442
|
+
title: "Share agentPickup",
|
|
443
|
+
description: "A QR code and link the user can share so friends can add agentPickup to Claude, ChatGPT or Grok and order too. " +
|
|
444
|
+
"Use when the user asks how to share agentPickup, invite friends, or get others using it. Name a shop to share it with that café picked.",
|
|
445
|
+
inputSchema: { ...shopArg },
|
|
446
|
+
annotations: { title: "Share agentPickup", readOnlyHint: true, openWorldHint: false },
|
|
447
|
+
_meta: SHARE_TOOL_META,
|
|
448
|
+
}, guarded(async (args) => {
|
|
449
|
+
const shop = args.shop_url ? await shopFor(args.shop_url) : undefined;
|
|
450
|
+
const name = shop ? await shopName(shop) : undefined;
|
|
451
|
+
const links = addLinks(shop);
|
|
452
|
+
const card = {
|
|
453
|
+
v: 1,
|
|
454
|
+
...links,
|
|
455
|
+
qrSvg: await QRCode.toString(links.url, { type: "svg", errorCorrectionLevel: "M", margin: 0, color: { dark: "#0a0b0dff", light: "#00000000" } }),
|
|
456
|
+
...(name ? { shopName: name } : {}),
|
|
457
|
+
};
|
|
458
|
+
return {
|
|
459
|
+
content: [
|
|
460
|
+
{
|
|
461
|
+
type: "text",
|
|
462
|
+
text: `Share this link: ${links.url}. A friend opens it (or scans its QR off the user's screen), adds agentPickup to Claude, ChatGPT or Grok, ` +
|
|
463
|
+
`then just asks for their usual${name ? ` at ${name}` : ""}. ${links.sharePageUrl} opens the phone's share sheet and has a sticker to print.`,
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
_meta: { [SHARE_META_KEY]: card },
|
|
467
|
+
};
|
|
312
468
|
}));
|
|
313
469
|
const closesIn = (g) => {
|
|
314
470
|
const minutes = Math.max(0, Math.round((g.closesAt - Date.now()) / 60_000));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const SHARE_META_KEY = "agentpickup/share";
|
|
2
|
+
export interface ShareCard {
|
|
3
|
+
v: 1;
|
|
4
|
+
/** The add page, maybe for one café (?shop=). */
|
|
5
|
+
url: string;
|
|
6
|
+
/** The add page's share mode: the share sheet and the sticker. */
|
|
7
|
+
sharePageUrl: string;
|
|
8
|
+
/** The QR as SVG, ink on transparent, for the white tile. */
|
|
9
|
+
qrSvg: string;
|
|
10
|
+
/** A café to name, when the link is for one. */
|
|
11
|
+
shopName?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function shareCardHtml(version: string): string;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// The share card (MCP Apps): what share_agentpickup shows when someone asks how to get friends using agentPickup.
|
|
2
|
+
// The counter sticker as a card: a QR to the add page that a friend scans off the screen, the link to copy, and
|
|
3
|
+
// buttons that open the add page's share mode (the phone's share sheet, and the printable sticker). Self-contained,
|
|
4
|
+
// like the pass card: no scripts, fonts or images from anywhere else, and the same small MCP Apps bridge.
|
|
5
|
+
export const SHARE_META_KEY = "agentpickup/share";
|
|
6
|
+
const PAGE = String.raw `<!doctype html>
|
|
7
|
+
<html lang="en">
|
|
8
|
+
<head>
|
|
9
|
+
<meta charset="utf-8">
|
|
10
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
11
|
+
<title>Share agentPickup</title>
|
|
12
|
+
<style>
|
|
13
|
+
:root{--bg:#0a0b0d;--text:#eef0f3;--muted:#b7bdc6;--dim:#8b929c;--line:#2a2f36;--lime:#b5f23d;--ink:#0a0b0d}
|
|
14
|
+
html,body{margin:0;padding:0;background:transparent}
|
|
15
|
+
body{font-family:var(--font-sans,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif);padding:2px;-webkit-font-smoothing:antialiased}
|
|
16
|
+
.card{box-sizing:border-box;max-width:420px;margin:0 auto;background:var(--bg);color:var(--text);border-radius:18px;padding:20px 20px 16px}
|
|
17
|
+
.top{display:flex;align-items:center;gap:8px;font-weight:700;font-size:15px}
|
|
18
|
+
.top svg{width:22px;height:22px;border-radius:5px}
|
|
19
|
+
.top .dim{color:var(--dim)}
|
|
20
|
+
h1{margin:14px 0 0;font-size:34px;line-height:.95;font-weight:850;letter-spacing:-.02em;text-transform:uppercase}
|
|
21
|
+
h1 em{font-style:normal;color:var(--lime)}
|
|
22
|
+
.sub{margin:8px 0 0;color:var(--muted);font-size:15px}
|
|
23
|
+
.row{display:flex;gap:16px;align-items:flex-end;margin-top:16px}
|
|
24
|
+
.tile{background:#fff;border-radius:14px;padding:12px;width:156px;height:156px;flex:none;box-sizing:border-box}
|
|
25
|
+
.tile svg{display:block;width:100%;height:100%}
|
|
26
|
+
ol{list-style:none;margin:0;padding:0;counter-reset:n;color:var(--muted);font-size:15px}
|
|
27
|
+
li{counter-increment:n;margin:0 0 8px;display:flex;align-items:center;gap:8px}
|
|
28
|
+
li::before{content:counter(n);width:22px;height:22px;border-radius:50%;border:1.5px solid var(--line);display:inline-flex;align-items:center;justify-content:center;font:600 12px ui-monospace,Menlo,monospace;color:var(--lime);flex:none}
|
|
29
|
+
.link{margin-top:14px;display:flex;gap:8px;align-items:center}
|
|
30
|
+
.link code{flex:1;min-width:0;overflow-wrap:anywhere;font:13px ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text);background:#16191d;border:1px solid var(--line);border-radius:10px;padding:9px 10px;user-select:all;-webkit-user-select:all}
|
|
31
|
+
button{font-weight:600;font-size:14px;font-family:inherit;border-radius:10px;padding:10px 14px;cursor:pointer;border:1px solid var(--line);background:transparent;color:var(--text);white-space:nowrap}
|
|
32
|
+
button.lime{background:var(--lime);color:var(--ink);border-color:var(--lime)}
|
|
33
|
+
.actions{display:flex;gap:8px;margin-top:10px;flex-wrap:wrap}
|
|
34
|
+
.actions button{flex:1}
|
|
35
|
+
.note{margin:10px 0 0;font-size:12px;color:var(--dim)}
|
|
36
|
+
@media (max-width:360px){.row{flex-direction:column;align-items:flex-start}}
|
|
37
|
+
</style>
|
|
38
|
+
</head>
|
|
39
|
+
<body>
|
|
40
|
+
<div id="root"></div>
|
|
41
|
+
<script>
|
|
42
|
+
(function () {
|
|
43
|
+
var VERSION = "__VERSION__";
|
|
44
|
+
var root = document.getElementById("root");
|
|
45
|
+
var state = null, canOpen = false, nextId = 1, pending = {}, lastHeight = -1;
|
|
46
|
+
var host = window.parent;
|
|
47
|
+
function send(msg) { try { host.postMessage(msg, "*"); } catch (e) {} }
|
|
48
|
+
function request(method, params) {
|
|
49
|
+
var id = nextId++;
|
|
50
|
+
send({ jsonrpc: "2.0", id: id, method: method, params: params });
|
|
51
|
+
return new Promise(function (resolve, reject) {
|
|
52
|
+
pending[id] = { resolve: resolve, reject: reject };
|
|
53
|
+
setTimeout(function () { if (pending[id]) { delete pending[id]; reject(new Error("timed out")); } }, 30000);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function notify(method, params) { send({ jsonrpc: "2.0", method: method, params: params }); }
|
|
57
|
+
window.addEventListener("message", function (e) {
|
|
58
|
+
if (host !== window && e.source !== host) return;
|
|
59
|
+
var m = e.data;
|
|
60
|
+
if (!m || m.jsonrpc !== "2.0") return;
|
|
61
|
+
if (m.id != null && !m.method) {
|
|
62
|
+
var p = pending[m.id];
|
|
63
|
+
if (p) { delete pending[m.id]; if (m.error) p.reject(m.error); else p.resolve(m.result); }
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (m.method === "ui/notifications/tool-result") return onResult(m.params);
|
|
67
|
+
if (m.method === "ui/notifications/host-context-changed") { applyContext(m.params || {}); return; }
|
|
68
|
+
if (m.method === "ui/resource-teardown" && m.id != null) return send({ jsonrpc: "2.0", id: m.id, result: {} });
|
|
69
|
+
if (m.method === "ping" && m.id != null) return send({ jsonrpc: "2.0", id: m.id, result: {} });
|
|
70
|
+
if (m.id != null && m.method && m.method.indexOf("ui/notifications/") !== 0) send({ jsonrpc: "2.0", id: m.id, error: { code: -32601, message: "Method not found" } });
|
|
71
|
+
});
|
|
72
|
+
function applyContext(ctx) {
|
|
73
|
+
var vars = ctx.styles && ctx.styles.variables;
|
|
74
|
+
if (vars && typeof vars["--font-sans"] === "string") document.documentElement.style.setProperty("--font-sans", vars["--font-sans"]);
|
|
75
|
+
}
|
|
76
|
+
function openLink(url) {
|
|
77
|
+
if (!url) return;
|
|
78
|
+
if (canOpen) request("ui/open-link", { url: url }).catch(function () { window.open(url, "_blank", "noopener"); });
|
|
79
|
+
else window.open(url, "_blank", "noopener");
|
|
80
|
+
}
|
|
81
|
+
function onResult(result) {
|
|
82
|
+
var data = result && result._meta && result._meta["agentpickup/share"];
|
|
83
|
+
if (data && data.url) state = data;
|
|
84
|
+
render();
|
|
85
|
+
}
|
|
86
|
+
function esc(s) { return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]; }); }
|
|
87
|
+
function safeSvg(svg) { return typeof svg === "string" && /^<svg[\s>]/.test(svg) && !/<script|<foreignObject|\son[a-z]+=/i.test(svg) ? svg : ""; }
|
|
88
|
+
function copy(text, button) {
|
|
89
|
+
var done = function () { button.textContent = "Copied"; setTimeout(function () { button.textContent = "Copy"; }, 1600); };
|
|
90
|
+
try {
|
|
91
|
+
if (navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(text).then(done, function () { select(); });
|
|
92
|
+
} catch (e) {}
|
|
93
|
+
select();
|
|
94
|
+
}
|
|
95
|
+
function select() {
|
|
96
|
+
var code = document.getElementById("url");
|
|
97
|
+
if (!code || !window.getSelection) return;
|
|
98
|
+
var range = document.createRange();
|
|
99
|
+
range.selectNodeContents(code);
|
|
100
|
+
var sel = window.getSelection();
|
|
101
|
+
sel.removeAllRanges();
|
|
102
|
+
sel.addRange(range);
|
|
103
|
+
}
|
|
104
|
+
function render() {
|
|
105
|
+
if (!state) { root.innerHTML = ""; return reportSize(); }
|
|
106
|
+
var svg = safeSvg(state.qrSvg);
|
|
107
|
+
var shown = state.url.replace(/^https?:\/\//, "");
|
|
108
|
+
root.innerHTML =
|
|
109
|
+
'<div class="card">' +
|
|
110
|
+
'<div class="top"><svg viewBox="0 0 32 32" aria-hidden="true"><rect width="32" height="32" fill="#16191d"/><rect x="7" y="7" width="7" height="7" rx="1.5" fill="#fff"/><rect x="18" y="7" width="7" height="7" rx="1.5" fill="#fff"/><rect x="7" y="18" width="7" height="7" rx="1.5" fill="#fff"/><path d="M18.5 21.5l2.5 2.5 4.5-5" fill="none" stroke="#b5f23d" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg><span><span class="dim">agent</span>Pickup</span></div>' +
|
|
111
|
+
"<h1>" + (state.shopName ? "Order from " + esc(state.shopName) + "<br>with your <em>AI</em>" : "Order ahead<br>from your <em>AI</em>") + "</h1>" +
|
|
112
|
+
'<p class="sub">Claude, ChatGPT or Grok</p>' +
|
|
113
|
+
'<div class="row">' + (svg ? '<div class="tile">' + svg.replace("<svg", '<svg role="img" aria-label="QR code to add agentPickup"') + "</div>" : "") +
|
|
114
|
+
"<ol><li>Scan</li><li>Add agentPickup</li><li>Ask for your usual</li></ol></div>" +
|
|
115
|
+
'<div class="link"><code id="url">' + esc(shown) + '</code><button type="button" id="copy">Copy</button></div>' +
|
|
116
|
+
'<div class="actions"><button class="lime" type="button" id="share">Share</button><button type="button" id="sticker">Get the sticker</button></div>' +
|
|
117
|
+
'<p class="note">A friend scans the QR off your screen, or you send them the link.</p>' +
|
|
118
|
+
"</div>";
|
|
119
|
+
document.getElementById("copy").addEventListener("click", function (e) { copy(state.url, e.currentTarget); });
|
|
120
|
+
document.getElementById("share").addEventListener("click", function () { openLink(state.sharePageUrl); });
|
|
121
|
+
document.getElementById("sticker").addEventListener("click", function () { openLink(state.sharePageUrl + (state.sharePageUrl.indexOf("?") >= 0 ? "&" : "?") + "sticker=1"); });
|
|
122
|
+
reportSize();
|
|
123
|
+
}
|
|
124
|
+
function reportSize() {
|
|
125
|
+
var height = Math.ceil(document.body.getBoundingClientRect().height);
|
|
126
|
+
if (height !== lastHeight) { lastHeight = height; notify("ui/notifications/size-changed", { height: height }); }
|
|
127
|
+
}
|
|
128
|
+
if (window.ResizeObserver) new ResizeObserver(reportSize).observe(document.body);
|
|
129
|
+
request("ui/initialize", { appInfo: { name: "agentpickup-share", version: VERSION }, appCapabilities: {}, protocolVersion: "2026-01-26" })
|
|
130
|
+
.then(function (r) {
|
|
131
|
+
canOpen = Boolean(r && r.hostCapabilities && r.hostCapabilities.openLinks);
|
|
132
|
+
applyContext((r && r.hostContext) || {});
|
|
133
|
+
notify("ui/notifications/initialized", {});
|
|
134
|
+
render();
|
|
135
|
+
}, function () { render(); });
|
|
136
|
+
})();
|
|
137
|
+
</script>
|
|
138
|
+
</body>
|
|
139
|
+
</html>`;
|
|
140
|
+
export function shareCardHtml(version) {
|
|
141
|
+
return PAGE.replace("__VERSION__", version);
|
|
142
|
+
}
|
package/package.json
CHANGED
|
@@ -1,62 +1,63 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@agentpickup/mcp",
|
|
3
|
-
"version": "0.
|
|
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
|
-
"mcpName": "io.github.dav3trad3r/agentpickup",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"main": "./dist/index.js",
|
|
8
|
-
"types": "./dist/index.d.ts",
|
|
9
|
-
"bin": {
|
|
10
|
-
"agentpickup-mcp": "dist/cli.js"
|
|
11
|
-
},
|
|
12
|
-
"exports": {
|
|
13
|
-
".": {
|
|
14
|
-
"source": "./src/index.ts",
|
|
15
|
-
"types": "./dist/index.d.ts",
|
|
16
|
-
"default": "./dist/index.js"
|
|
17
|
-
}
|
|
18
|
-
},
|
|
19
|
-
"files": [
|
|
20
|
-
"dist",
|
|
21
|
-
"LICENSE.md",
|
|
22
|
-
"README.md"
|
|
23
|
-
],
|
|
24
|
-
"scripts": {
|
|
25
|
-
"build": "tsc -p tsconfig.build.json",
|
|
26
|
-
"prepublishOnly": "npm run build"
|
|
27
|
-
},
|
|
28
|
-
"engines": {
|
|
29
|
-
"node": ">=22.13"
|
|
30
|
-
},
|
|
31
|
-
"keywords": [
|
|
32
|
-
"mcp",
|
|
33
|
-
"model-context-protocol",
|
|
34
|
-
"x402",
|
|
35
|
-
"agentic-commerce",
|
|
36
|
-
"agent-payments",
|
|
37
|
-
"click-and-collect",
|
|
38
|
-
"pickup"
|
|
39
|
-
],
|
|
40
|
-
"repository": {
|
|
41
|
-
"type": "git",
|
|
42
|
-
"url": "git+https://github.com/Tokenise-au/agentpickup-sdk.git",
|
|
43
|
-
"directory": "packages/mcp"
|
|
44
|
-
},
|
|
45
|
-
"homepage": "https://agentpickup.app",
|
|
46
|
-
"bugs": {
|
|
47
|
-
"url": "https://github.com/Tokenise-au/agentpickup-sdk/issues"
|
|
48
|
-
},
|
|
49
|
-
"author": "Dave Hallett",
|
|
50
|
-
"license": "FSL-1.1-ALv2",
|
|
51
|
-
"publishConfig": {
|
|
52
|
-
"access": "public"
|
|
53
|
-
},
|
|
54
|
-
"dependencies": {
|
|
55
|
-
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
56
|
-
"@x402/core": "2.29.0",
|
|
57
|
-
"@x402/evm": "2.29.0",
|
|
58
|
-
"@x402/extensions": "2.29.0",
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
|
|
62
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentpickup/mcp",
|
|
3
|
+
"version": "0.4.1",
|
|
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
|
+
"mcpName": "io.github.dav3trad3r/agentpickup",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"agentpickup-mcp": "dist/cli.js"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"source": "./src/index.ts",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"LICENSE.md",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc -p tsconfig.build.json",
|
|
26
|
+
"prepublishOnly": "npm run build"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=22.13"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"mcp",
|
|
33
|
+
"model-context-protocol",
|
|
34
|
+
"x402",
|
|
35
|
+
"agentic-commerce",
|
|
36
|
+
"agent-payments",
|
|
37
|
+
"click-and-collect",
|
|
38
|
+
"pickup"
|
|
39
|
+
],
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/Tokenise-au/agentpickup-sdk.git",
|
|
43
|
+
"directory": "packages/mcp"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://agentpickup.app",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/Tokenise-au/agentpickup-sdk/issues"
|
|
48
|
+
},
|
|
49
|
+
"author": "Dave Hallett",
|
|
50
|
+
"license": "FSL-1.1-ALv2",
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
56
|
+
"@x402/core": "2.29.0",
|
|
57
|
+
"@x402/evm": "2.29.0",
|
|
58
|
+
"@x402/extensions": "2.29.0",
|
|
59
|
+
"qrcode": "^1.5.4",
|
|
60
|
+
"viem": "^2.57.14",
|
|
61
|
+
"zod": "^4.6.7"
|
|
62
|
+
}
|
|
63
|
+
}
|