@agentpickup/mcp 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,18 +10,28 @@ 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 QR. 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 |
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
20
  | `close_group_order` | Closes a group order early, as its organiser; the shop gets one combined order and the organiser its QR |
21
21
 
22
+ ## The pickup pass card
23
+
24
+ In chat apps that draw [MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview) cards (Claude and ChatGPT
25
+ among them), `create_pickup_order`, `order_status` and `get_pickup_pass` show the order as a card: a Pay button while
26
+ it's unpaid, then the QR and pickup code, kept up to date until it's collected. When the shop marks it ready, the QR's
27
+ tile turns lime. The card is one self-contained page (`ui://agentpickup/pass-<version>.html`) that loads nothing from
28
+ anywhere else. In apps without cards, the agent gives the user the pass link and pickup code. The shop emails the pass
29
+ either way.
30
+
22
31
  ## Use it
23
32
 
24
- Try it against the hosted demo shop first. It pays with the demo's own small wallet, within the demo's caps:
33
+ Try it against the hosted demo shops first. The hosted server holds no wallet: its demo shops take a free test
34
+ payment, the Square sandbox's test card, or USDC from the user's own x402 wallet:
25
35
 
26
36
  ```json
27
37
  { "mcpServers": { "agentpickup": { "type": "http", "url": "https://agentpickup.app/mcp" } } }
@@ -62,6 +72,7 @@ items and the total with the user before paying.
62
72
  import { createMcpHandler, walletPayer } from "@agentpickup/mcp";
63
73
 
64
74
  const mcp = createMcpHandler({ shopUrl: "https://orders.yourshop.com" }); // no wallet: pay_order quotes
75
+ // `instructions` adds your own notes to what agents read on connecting, e.g. which of your shops take what.
65
76
  app.all("/mcp", (c) => mcp(c.req.raw)); // Streamable HTTP, stateless, any Request/Response runtime
66
77
  ```
67
78
 
package/dist/card.d.ts ADDED
@@ -0,0 +1,25 @@
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
+ }
24
+ /** The card's page. Its version goes in the handshake, so a host's logs say which card it ran. */
25
+ export declare function passCardHtml(version: string): string;
package/dist/card.js ADDED
@@ -0,0 +1,269 @@
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;text-align:right}
58
+ @media (prefers-reduced-motion:reduce){.pour{transition:none}.wrap.ready::after{animation:none;opacity:1}}
59
+ </style>
60
+ </head>
61
+ <body>
62
+ <div id="root"></div>
63
+ <script>
64
+ (function () {
65
+ var VERSION = "__VERSION__";
66
+ var LABELS = { awaiting_payment: "Waiting for payment", paid: "Paid", ready: "Ready", collected: "Collected", expired: "Expired", cancelled: "Cancelled" };
67
+ var FINAL = { collected: 1, expired: 1, cancelled: 1 };
68
+ var root = document.getElementById("root");
69
+ var state = null, canCall = false, canOpen = false, locale, timeZone;
70
+ var nextId = 1, pending = {}, timer = null, started = Date.now(), lastHtml = "", poured = false, lastHeight = -1, stopped = false;
71
+
72
+ // ---------- the bridge ----------
73
+ var host = window.parent;
74
+ function send(msg) { try { host.postMessage(msg, "*"); } catch (e) {} }
75
+ function request(method, params) {
76
+ var id = nextId++;
77
+ send({ jsonrpc: "2.0", id: id, method: method, params: params });
78
+ return new Promise(function (resolve, reject) {
79
+ pending[id] = { resolve: resolve, reject: reject };
80
+ setTimeout(function () { if (pending[id]) { delete pending[id]; reject(new Error("timed out")); } }, 30000);
81
+ });
82
+ }
83
+ function notify(method, params) { send({ jsonrpc: "2.0", method: method, params: params }); }
84
+ window.addEventListener("message", function (e) {
85
+ if (host !== window && e.source !== host) return;
86
+ var m = e.data;
87
+ if (!m || m.jsonrpc !== "2.0") return;
88
+ if (m.id != null && !m.method) {
89
+ var p = pending[m.id];
90
+ if (p) { delete pending[m.id]; if (m.error) p.reject(m.error); else p.resolve(m.result); }
91
+ return;
92
+ }
93
+ if (m.method === "ui/notifications/tool-result") return onResult(m.params);
94
+ if (m.method === "ui/notifications/host-context-changed") { applyContext(m.params || {}); return render(); }
95
+ if (m.method === "ui/resource-teardown") { stop(); if (m.id != null) send({ jsonrpc: "2.0", id: m.id, result: {} }); return; }
96
+ if (m.method === "ping" && m.id != null) return send({ jsonrpc: "2.0", id: m.id, result: {} });
97
+ if (m.id != null && m.method && m.method.indexOf("ui/notifications/") !== 0) {
98
+ send({ jsonrpc: "2.0", id: m.id, error: { code: -32601, message: "Method not found" } });
99
+ }
100
+ });
101
+ function applyContext(ctx) {
102
+ if (ctx.theme === "light" || ctx.theme === "dark") document.documentElement.setAttribute("data-theme", ctx.theme);
103
+ var vars = ctx.styles && ctx.styles.variables;
104
+ if (vars) for (var k in vars) if (k.indexOf("--") === 0 && typeof vars[k] === "string") document.documentElement.style.setProperty(k, vars[k]);
105
+ if (ctx.locale) locale = ctx.locale;
106
+ if (ctx.timeZone) timeZone = ctx.timeZone;
107
+ }
108
+ function openLink(url) {
109
+ if (!url) return;
110
+ if (canOpen) request("ui/open-link", { url: url }).catch(function () { window.open(url, "_blank", "noopener"); });
111
+ else window.open(url, "_blank", "noopener");
112
+ }
113
+
114
+ // ---------- results ----------
115
+ function cardOf(result) {
116
+ if (!result) return null;
117
+ var meta = result._meta && result._meta["agentpickup/pass"];
118
+ if (meta && meta.orderId) return meta;
119
+ // A host that drops _meta: the JSON after the tool's summary line still has the basics.
120
+ try {
121
+ var text = result.content && result.content[0] && result.content[0].text;
122
+ var data = JSON.parse(text.slice(text.indexOf("\n\n") + 2));
123
+ var id = data.orderId || data.id || (data.order && data.order.id);
124
+ if (!id) return null;
125
+ return { orderId: id, status: data.status, passUrl: data.passUrl, pickupCode: data.pickupCode, checkoutUrl: data.checkoutUrl, total: data.total, claim: data.claim, shopUrl: data.shop };
126
+ } catch (e) { return null; }
127
+ }
128
+ function onResult(result) {
129
+ var card = cardOf(result);
130
+ if (card) {
131
+ if (state && state.orderId !== card.orderId) state = null;
132
+ var next = state ? Object.assign({}, state) : {};
133
+ for (var k in card) if (card[k] !== undefined && card[k] !== null && card[k] !== "") next[k] = card[k];
134
+ if (card.status && card.status !== "awaiting_payment") delete next.checkoutUrl;
135
+ state = next;
136
+ }
137
+ render();
138
+ schedule();
139
+ }
140
+
141
+ // ---------- keeping up to date ----------
142
+ function stop() { stopped = true; clearTimeout(timer); }
143
+ function schedule() {
144
+ clearTimeout(timer);
145
+ if (stopped || !state || !state.claim || !canCall || FINAL[state.status]) return;
146
+ if (Date.now() - started > 4 * 3600 * 1000) return;
147
+ var wait = document.hidden ? 60000 : state.status === "awaiting_payment" ? 5000 : 15000;
148
+ timer = setTimeout(poll, wait);
149
+ }
150
+ function poll() {
151
+ var args = { order_id: state.orderId, claim: state.claim };
152
+ if (state.shopUrl) args.shop_url = state.shopUrl;
153
+ request("tools/call", { name: "order_status", arguments: args }).then(function (r) {
154
+ if (r && !r.isError) onResult(r); else schedule();
155
+ }, schedule);
156
+ }
157
+ document.addEventListener("visibilitychange", function () { if (!document.hidden && state) { clearTimeout(timer); poll(); } });
158
+
159
+ // ---------- drawing ----------
160
+ function esc(s) { return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) { return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]; }); }
161
+ function time(ms) {
162
+ var d = new Date(ms), o = { hour: "numeric", minute: "2-digit" };
163
+ if (timeZone) o.timeZone = timeZone;
164
+ try {
165
+ var s = d.toLocaleTimeString(locale, o);
166
+ if (Math.abs(ms - Date.now()) > 18 * 3600 * 1000) {
167
+ var od = { weekday: "short" };
168
+ if (timeZone) od.timeZone = timeZone;
169
+ s = d.toLocaleDateString(locale, od) + " " + s;
170
+ }
171
+ return s;
172
+ } catch (e) { return d.toLocaleTimeString(); }
173
+ }
174
+ function safeSvg(svg) { return typeof svg === "string" && /^<svg[\s>]/.test(svg) && !/<script|<foreignObject|\son[a-z]+=/i.test(svg) ? svg : ""; }
175
+ function message(s) {
176
+ var live = canCall && s.claim;
177
+ switch (s.status) {
178
+ case "awaiting_payment":
179
+ return s.checkoutUrl
180
+ ? "Pay on the checkout page. " + (live ? "This card turns into your pickup pass once it's paid." : "Then ask for the pickup pass.")
181
+ : "Waiting for the payment. " + (live ? "This card turns into your pickup pass once it's paid." : "");
182
+ case "paid": return "Paid. " + (live ? "This turns green when it's ready. " : "") + "The pass is in your email too.";
183
+ case "ready": return "Ready at the counter. Show the QR or say the code.";
184
+ case "collected": return s.collectedAt ? "Collected at " + time(s.collectedAt) + ". Enjoy." : "Collected. Enjoy.";
185
+ case "expired": return "This order expired before anyone collected it.";
186
+ case "cancelled": return "This order was cancelled.";
187
+ default: return "";
188
+ }
189
+ }
190
+ function view(s) {
191
+ var status = s.status || "awaiting_payment";
192
+ var label = LABELS[status] || status;
193
+ var live = status === "paid" || status === "ready";
194
+ var h = '<div class="card">';
195
+ h += '<div class="head"><div><p class="shop">' + esc(s.shopName || "Your pickup order") + "</p>";
196
+ if (s.items) h += '<p class="items">' + esc(s.items) + (s.total ? " · " + esc(s.total) : "") + "</p>";
197
+ h += '</div><span class="badge' + (status === "ready" ? " ready" : "") + '">' + esc(label) + "</span></div>";
198
+ var svg = safeSvg(s.qrSvg);
199
+ if (live && svg) {
200
+ h += '<div class="wrap' + (status === "ready" && poured ? " ready" : "") + '" id="wrap"><div class="tile"><div class="pour"></div>' +
201
+ svg.replace("<svg", '<svg role="img" aria-label="Pickup QR code"') + "</div></div>";
202
+ } else if (status === "collected") {
203
+ h += '<div class="wrap"><div class="tile plain"><div class="done"><b>&#10003;</b>Collected</div></div></div>';
204
+ }
205
+ if (live && (s.pickupCode || s.collectBy)) {
206
+ h += '<div class="facts">';
207
+ if (s.pickupCode) h += '<div class="fact"><span>Pickup code</span><b class="code">' + esc(s.pickupCode) + "</b></div>";
208
+ if (s.collectBy) h += '<div class="fact"><span>Collect by</span><b>' + esc(time(s.collectBy)) + "</b></div>";
209
+ h += "</div>";
210
+ }
211
+ if (live || status === "collected") {
212
+ var at = { paid: 0, ready: 1, collected: 2 }[status];
213
+ h += '<div class="steps">' + ["Paid", "Ready", "Collected"].map(function (name, i) {
214
+ return '<span class="step' + (i < at ? " past" : i === at ? " now" : "") + '"><span class="dot"></span>' + name + "</span>";
215
+ }).join("") + "</div>";
216
+ }
217
+ var msg = message(s);
218
+ if (msg) h += '<p class="msg">' + esc(msg) + "</p>";
219
+ if (s.rail === "test") h += '<p class="test">Test order: no money moved.</p>';
220
+ var actions = "";
221
+ if (status === "awaiting_payment" && s.checkoutUrl) actions += '<button class="pay" data-open="checkoutUrl">Pay' + (s.total ? " " + esc(s.total) : "") + " by card</button>";
222
+ if (live && s.passUrl) actions += '<button class="link" data-open="passUrl">Open the pass</button>';
223
+ if (actions) h += '<div class="row">' + actions + "</div>";
224
+ h += '<div class="foot">agentPickup</div></div>';
225
+ return h;
226
+ }
227
+ function render() {
228
+ var html = state ? view(state) : "";
229
+ if (html !== lastHtml) {
230
+ root.innerHTML = html;
231
+ lastHtml = html;
232
+ var buttons = root.querySelectorAll("[data-open]");
233
+ for (var i = 0; i < buttons.length; i++) buttons[i].addEventListener("click", function (e) { openLink(state[e.currentTarget.getAttribute("data-open")]); });
234
+ }
235
+ // The lime pours in once, the first time the card sees the order ready.
236
+ var wrap = document.getElementById("wrap");
237
+ if (wrap && state && state.status === "ready" && !poured) {
238
+ void wrap.offsetWidth;
239
+ wrap.classList.add("ready");
240
+ poured = true;
241
+ lastHtml = view(state);
242
+ }
243
+ reportSize();
244
+ }
245
+ function reportSize() {
246
+ var height = Math.ceil(document.body.getBoundingClientRect().height);
247
+ if (height !== lastHeight) { lastHeight = height; notify("ui/notifications/size-changed", { height: height }); }
248
+ }
249
+ if (window.ResizeObserver) new ResizeObserver(reportSize).observe(document.body);
250
+
251
+ // ---------- start ----------
252
+ request("ui/initialize", { appInfo: { name: "agentpickup-pass", version: VERSION }, appCapabilities: {}, protocolVersion: "2026-01-26" })
253
+ .then(function (r) {
254
+ var caps = (r && r.hostCapabilities) || {};
255
+ canCall = Boolean(caps.serverTools);
256
+ canOpen = Boolean(caps.openLinks);
257
+ applyContext((r && r.hostContext) || {});
258
+ notify("ui/notifications/initialized", {});
259
+ render();
260
+ schedule();
261
+ }, function () { render(); });
262
+ })();
263
+ </script>
264
+ </body>
265
+ </html>`;
266
+ /** The card's page. Its version goes in the handshake, so a host's logs say which card it ran. */
267
+ export function passCardHtml(version) {
268
+ return PAGE.replace("__VERSION__", version);
269
+ }
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.2";
3
+ export declare const VERSION = "0.4.0";
4
4
  export interface PickupMcpOptions {
5
5
  /** The agentPickup shop this server orders from. */
6
6
  shopUrl: string;
@@ -20,6 +20,8 @@ export interface PickupMcpOptions {
20
20
  fetch?: Fetch;
21
21
  /** The wallet that pays. Without one, pay_order returns the price and pay link for another x402 client. */
22
22
  payer?: Payer;
23
+ /** Notes added to the instructions agents read on connecting, e.g. which of this host's shops take what. */
24
+ instructions?: string;
23
25
  }
24
26
  export declare function createPickupMcpServer(opts: PickupMcpOptions): McpServer;
25
27
  /** A Streamable HTTP endpoint (stateless, JSON responses) for hosting the server, e.g. at /mcp. */
package/dist/server.js CHANGED
@@ -2,25 +2,48 @@
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";
6
8
  import { errorOf, usd } from "./wallet.js";
7
- export const VERSION = "0.3.2";
9
+ export const VERSION = "0.4.0";
10
+ // The pickup pass card (MCP Apps). The version is in the URI so hosts that cache cards pick up a new one.
11
+ const CARD_URI = `ui://agentpickup/pass-${VERSION}.html`;
12
+ const CARD_TOOL_META = { ui: { resourceUri: CARD_URI }, "ui/resourceUri": CARD_URI };
13
+ // Shop names for the card, so polling order_status needn't read the catalog each time. Shared across the
14
+ // per-request servers a hosted endpoint makes.
15
+ const shopNames = new Map();
16
+ const rememberShop = (shop, name) => {
17
+ if (typeof name !== "string" || !name)
18
+ return;
19
+ if (shopNames.size > 500)
20
+ shopNames.clear();
21
+ shopNames.set(shop, name);
22
+ };
23
+ /** The QR in ink on transparent: the card's tile behind it is white, or lime once the order is ready. */
24
+ const qrSvg = (content) => QRCode.toString(content, { type: "svg", errorCorrectionLevel: "M", margin: 0, color: { dark: "#0a0b0dff", light: "#00000000" } });
8
25
  class ToolError extends Error {
9
26
  }
10
27
  const INSTRUCTIONS = `agentPickup lets you buy things online for the user to collect in person.
11
28
  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
29
  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, where the shop sends the pickup QR.
14
- 3. pay_order: pays from the configured wallet in USDC over x402. Where the shop takes cards, payment: "card" on
15
- create_pickup_order gives a checkout link for the user to pay on instead. Confirm the items and total with the user first.
16
- 4. Give the user the pass link and pickup code. They show the QR at the counter to collect.
17
- get_pickup_pass fetches the pass again later; order_status says whether it's ready or collected.
30
+ 2. create_pickup_order: needs the user's email; the shop emails the pickup pass there too. It returns a claim:
31
+ keep it with the order id.
32
+ 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
33
+ own x402 wallet. Where the shop takes cards, payment: "card" on
34
+ create_pickup_order gives a checkout link for the user to pay on instead. Where it takes test payments (get_shop says
35
+ so), payment: "test" runs the whole flow with no money moving. Confirm the items and total with the user first.
36
+ 4. The pass: where the chat shows cards, the order appears as a card that becomes the pickup pass (QR and code)
37
+ once it's paid and keeps itself up to date until it's collected. Otherwise give the user the pass link and pickup
38
+ code. They show the QR at the counter, or say the code, to collect.
39
+ get_pickup_pass (with the claim) fetches the pass again; order_status (with the claim) says whether it's ready.
18
40
  Group orders (a coffee run): start_group_order gives a code to share; each person's agent calls join_group_order and
19
41
  pays its own share with pay_order; at closing time the shop gets one combined order and the organiser gets its QR.
20
42
  group_order_status shows who's in; close_group_order closes it early.`;
21
43
  const money = (cents, currency) => new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(cents / 100);
22
- const reply = (summary, data) => ({
44
+ const reply = (summary, data, card) => ({
23
45
  content: [{ type: "text", text: `${summary}\n\n${JSON.stringify(data, null, 2)}` }],
46
+ ...(card ? { _meta: { [CARD_META_KEY]: card } } : {}),
24
47
  });
25
48
  const failure = (message) => ({ content: [{ type: "text", text: message }], isError: true });
26
49
  /** A shop's base URL: shops can live under a path (https://host/v/cafe). A pasted API or pass link works too. */
@@ -71,6 +94,44 @@ export function createPickupMcpServer(opts) {
71
94
  return failure(`Couldn't reach the shop: ${err instanceof Error ? err.message : String(err)}`);
72
95
  }
73
96
  };
97
+ /** The pass, for whoever made the order. null while it isn't paid (or is a share of a group order). */
98
+ async function passByClaim(shop, orderId, claim) {
99
+ const res = await doFetch(new Request(`${shop}/api/orders/${encodeURIComponent(orderId)}/pass`, { headers: { "X-Pickup-Claim": claim } }));
100
+ if (res.status === 409)
101
+ return null;
102
+ if (!res.ok)
103
+ throw new ToolError(await errorOf(res, `The shop answered ${res.status}.`));
104
+ return (await res.json());
105
+ }
106
+ async function shopName(shop) {
107
+ if (!shopNames.has(shop)) {
108
+ const data = await call(shop, "/api/catalog").catch(() => null);
109
+ rememberShop(shop, data?.merchant?.name);
110
+ }
111
+ return shopNames.get(shop);
112
+ }
113
+ /** What the pass card draws for an order, with its QR once there's a pass. */
114
+ async function cardFor(shop, order, extra = {}, pass) {
115
+ return {
116
+ v: 1,
117
+ orderId: order.id,
118
+ shopUrl: shop,
119
+ shopName: shopNames.get(shop),
120
+ status: order.status,
121
+ rail: order.rail,
122
+ items: order.items.map((i) => `${i.qty}× ${i.name}`).join(", "),
123
+ total: money(order.totalCents, order.currency),
124
+ collectBy: order.collectBy,
125
+ collectedAt: order.collectedAt ?? null,
126
+ ...extra,
127
+ ...(pass ? { passUrl: pass.passUrl, pickupCode: pass.pickupCode, qrSvg: await qrSvg(pass.qrContent) } : {}),
128
+ };
129
+ }
130
+ const claimArg = z
131
+ .string()
132
+ .max(200)
133
+ .optional()
134
+ .describe("The claim create_pickup_order returned with the order. With it, a paid order's pass comes back too.");
74
135
  const shopArg = opts.allowOtherShops || opts.directoryUrl
75
136
  ? { shop_url: z.string().optional().describe(`An agentPickup shop's URL, as find_shops returns it. Defaults to ${home}.`) }
76
137
  : {};
@@ -80,7 +141,15 @@ export function createPickupMcpServer(opts) {
80
141
  version: VERSION,
81
142
  websiteUrl: "https://agentpickup.app",
82
143
  icons: [{ src: "https://agentpickup.app/logo.png", mimeType: "image/png", sizes: ["1024x1024"] }],
83
- }, { instructions: INSTRUCTIONS });
144
+ }, { instructions: opts.instructions ? `${INSTRUCTIONS}\n${opts.instructions}` : INSTRUCTIONS });
145
+ server.registerResource("pickup-pass", CARD_URI, {
146
+ title: "Pickup pass",
147
+ 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.",
148
+ mimeType: CARD_MIME_TYPE,
149
+ _meta: { ui: { prefersBorder: false } },
150
+ }, async () => ({
151
+ contents: [{ uri: CARD_URI, mimeType: CARD_MIME_TYPE, text: passCardHtml(VERSION), _meta: { ui: { prefersBorder: false } } }],
152
+ }));
84
153
  if (opts.directoryUrl) {
85
154
  const directoryUrl = opts.directoryUrl;
86
155
  server.registerTool("find_shops", {
@@ -152,6 +221,7 @@ export function createPickupMcpServer(opts) {
152
221
  const shop = await shopFor(args.shop_url);
153
222
  const data = await call(shop, "/api/catalog");
154
223
  const merchant = data.merchant;
224
+ rememberShop(shop, merchant.name);
155
225
  const currency = String(data.currency ?? "usd");
156
226
  const items = data.items.map((i) => ({
157
227
  id: i.id,
@@ -175,32 +245,42 @@ export function createPickupMcpServer(opts) {
175
245
  }));
176
246
  server.registerTool("create_pickup_order", {
177
247
  title: "Order for pickup",
178
- description: "Create an order to collect in person. Returns the order id and total; pay it with pay_order. " +
179
- "The shop emails the pickup QR to `email` once it's paid. Where the shop takes cards, `payment: \"card\"` " +
180
- "returns a checkout link for the user to pay on instead.",
248
+ description: "Create an order to collect in person. Returns the order id, total and a claim (keep it); pay it with pay_order. " +
249
+ "The shop emails the pickup pass to `email` once it's paid, and where the chat shows cards the order shows as one " +
250
+ "that becomes the pass. Where the shop takes cards, `payment: \"card\"` returns a checkout link for the user to pay on instead.",
181
251
  inputSchema: {
182
252
  items: z
183
253
  .array(z.object({ id: z.string().describe("Item id from get_shop"), qty: z.number().int().min(1).max(20) }))
184
254
  .min(1),
185
- email: z.string().email().describe("The user's email. The pickup QR goes here."),
255
+ email: z.string().email().describe("The user's email. The pickup pass goes here too."),
186
256
  name: z.string().max(100).optional().describe("Name for the order, shown to staff"),
187
257
  payment: z
188
- .enum(["agent", "card"])
258
+ .enum(["agent", "card", "test"])
189
259
  .optional()
190
- .describe("Who pays: agent (USDC over x402, or a test payment at a test shop) or card (the user pays on a checkout link). Default: agent where the shop takes it, else card."),
260
+ .describe("Who pays: agent (USDC over x402, or a test payment at a test shop), card (the user pays on a checkout link), " +
261
+ "or test (where the shop takes test payments: the whole flow with no money moving). Default: agent where the shop takes it, else card."),
191
262
  ...shopArg,
192
263
  },
193
264
  annotations: { title: "Order for pickup", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
265
+ _meta: CARD_TOOL_META,
194
266
  }, guarded(async (args) => {
195
267
  const shop = await shopFor(args.shop_url);
196
268
  // The agent pays over x402 where the shop takes it, a test shop marks the order paid straight away, and a
197
269
  // shop that takes cards gives the user a checkout link.
198
- const { payments } = (await call(shop, "/api/catalog"));
270
+ const catalog = (await call(shop, "/api/catalog"));
271
+ rememberShop(shop, catalog.merchant?.name);
272
+ const { payments } = catalog;
199
273
  const agentRail = payments.x402 ? "x402" : payments.test ? "test" : null;
200
274
  const cardRail = payments.square ? "square" : payments.card ? "card" : null;
201
- const rail = args.payment === "card" ? cardRail : args.payment === "agent" ? agentRail : (agentRail ?? cardRail);
202
- if (!rail)
203
- throw new ToolError(args.payment === "card" ? "This shop doesn't take cards." : "This shop doesn't take agent payments.");
275
+ const rail = args.payment === "test" ? (payments.test ? "test" : null)
276
+ : args.payment === "card" ? cardRail
277
+ : args.payment === "agent" ? agentRail
278
+ : (agentRail ?? cardRail);
279
+ if (!rail) {
280
+ throw new ToolError(args.payment === "test" ? "This shop doesn't take test payments."
281
+ : args.payment === "card" ? "This shop doesn't take cards."
282
+ : "This shop doesn't take agent payments.");
283
+ }
204
284
  const data = await call(shop, "/api/orders", {
205
285
  method: "POST",
206
286
  headers: { "Content-Type": "application/json" },
@@ -208,16 +288,31 @@ export function createPickupMcpServer(opts) {
208
288
  });
209
289
  const order = data.order;
210
290
  const next = data.next;
291
+ // Older shops don't hand out claims; everything else still works.
292
+ const claim = typeof data.claim === "string" ? data.claim : undefined;
293
+ const what = `${order.items.map((i) => `${i.qty}× ${i.name}`).join(", ")}, ${money(order.totalCents, order.currency)}`;
294
+ const keep = claim ? " Keep the claim: get_pickup_pass and order_status take it." : "";
211
295
  if (rail === "test") {
212
- return reply(`Test order ${order.id}: ${order.items.map((i) => `${i.qty}× ${i.name}`).join(", ")}, ${money(order.totalCents, order.currency)}, ` +
213
- `marked paid with no money moving. The shop emails ${args.email} the receipt and pickup QR; the pass is at ${next.passUrl}.`, { shop, orderId: order.id, status: order.status, test: true, total: money(order.totalCents, order.currency), items: order.items, passUrl: next.passUrl });
296
+ const pass = claim ? await passByClaim(shop, order.id, claim).catch(() => null) : null;
297
+ return reply(`Test order ${order.id}: ${what}, marked paid with no money moving. The pickup pass is at ${next.passUrl}` +
298
+ (pass ? ` (pickup code ${pass.pickupCode})` : "") +
299
+ `, and the shop emails it to ${args.email}.${keep}`, {
300
+ shop,
301
+ orderId: order.id,
302
+ claim,
303
+ status: order.status,
304
+ test: true,
305
+ total: money(order.totalCents, order.currency),
306
+ items: order.items,
307
+ passUrl: next.passUrl,
308
+ pickupCode: pass?.pickupCode,
309
+ }, await cardFor(shop, order, { claim, rail }, pass));
214
310
  }
215
311
  if (rail === "square" || rail === "card") {
216
- return reply(`Order ${order.id}: ${order.items.map((i) => `${i.qty}× ${i.name}`).join(", ")}, ${money(order.totalCents, order.currency)}. ` +
217
- `Give the user this link to pay by card: ${next.url}. Once they've paid, the shop emails ${args.email} the receipt and pickup QR.`, { shop, orderId: order.id, total: money(order.totalCents, order.currency), items: order.items, payBy: "card", checkoutUrl: next.url, collectBy: order.collectBy });
312
+ return reply(`Order ${order.id}: ${what}. Give the user this link to pay by card: ${next.url}. Once they've paid, the shop emails ` +
313
+ `${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 }));
218
314
  }
219
- return reply(`Order ${order.id}: ${order.items.map((i) => `${i.qty}× ${i.name}`).join(", ")}, ${money(order.totalCents, order.currency)}. ` +
220
- `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 });
315
+ 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 }));
221
316
  }));
222
317
  server.registerTool("pay_order", {
223
318
  title: "Pay for an order",
@@ -245,24 +340,39 @@ export function createPickupMcpServer(opts) {
245
340
  const header = res.headers.get("PAYMENT-REQUIRED");
246
341
  const required = (header ? JSON.parse(Buffer.from(header, "base64").toString("utf8")) : await res.json());
247
342
  const accept = required.accepts?.[0];
248
- return reply(`Waiting for payment: ${usd(accept?.amount)} USDC on ${accept?.network}. No wallet is configured here; ` +
249
- `POST the pay link with any x402 client, or set AGENTPICKUP_WALLET_KEY.`, { orderId: args.order_id, payUrl, amount: usd(accept?.amount), network: accept?.network, asset: accept?.asset, payTo: accept?.payTo });
343
+ // Other ways to finish, for an agent with no wallet of its own (a chat app on the web, say).
344
+ const { payments } = (await call(shop, "/api/catalog").catch(() => ({ payments: {} })));
345
+ const others = [
346
+ ...(payments.square || payments.card ? ['payment "card" gives the user a checkout link to pay on'] : []),
347
+ ...(payments.test ? ['payment "test" runs the whole flow with no money moving'] : []),
348
+ ];
349
+ return reply(`Waiting for payment: ${usd(accept?.amount)} USDC on ${accept?.network}. No wallet is set up in this server: pay the link ` +
350
+ `from the user's own x402 wallet (such as Coinbase's agent wallet), or set AGENTPICKUP_WALLET_KEY where this server runs.` +
351
+ (others.length ? ` Or create the order again with create_pickup_order: ${others.join("; ")}.` : ""), { orderId: args.order_id, payUrl, amount: usd(accept?.amount), network: accept?.network, asset: accept?.asset, payTo: accept?.payTo });
250
352
  }
251
353
  const paid = await opts.payer.pay({ id: args.order_id, payUrl }, doFetch);
252
- if (!paid.ok)
253
- throw new ToolError(paid.error);
354
+ if (!paid.ok) {
355
+ // A demo or test shop can still show the user the whole flow, with a test payment instead.
356
+ const { payments } = (await call(shop, "/api/catalog").catch(() => ({ payments: {} })));
357
+ throw new ToolError(paid.error +
358
+ (payments.test
359
+ ? ' This shop also takes test payments, which move no money: call create_pickup_order again with payment "test" to run the whole flow.'
360
+ : ""));
361
+ }
254
362
  return reply(`Paid ${paid.amount ?? ""} USDC. Pickup pass: ${paid.passUrl}` +
255
363
  (paid.emailedTo ? ` (also emailed to ${paid.emailedTo})` : "") +
256
364
  `. The user shows its QR at the counter.`, { orderId: args.order_id, ...paid, ok: undefined });
257
365
  }));
258
366
  server.registerTool("order_status", {
259
367
  title: "Check an order",
260
- description: "Whether an order is waiting for payment, paid, ready to collect, collected or expired.",
261
- inputSchema: { order_id: z.string(), ...shopArg },
368
+ description: "Whether an order is waiting for payment, paid, ready to collect, collected or expired. With the order's claim, " +
369
+ "a paid order's pass comes back too, and the chat's card for it updates.",
370
+ inputSchema: { order_id: z.string(), claim: claimArg, ...shopArg },
262
371
  annotations: { title: "Check an order", readOnlyHint: true, openWorldHint: true },
372
+ _meta: CARD_TOOL_META,
263
373
  }, guarded(async (args) => {
264
374
  const shop = await shopFor(args.shop_url);
265
- const { order } = (await call(shop, `/api/orders/${encodeURIComponent(args.order_id)}`));
375
+ const { order, checkoutUrl } = (await call(shop, `/api/orders/${encodeURIComponent(args.order_id)}`));
266
376
  const words = {
267
377
  awaiting_payment: "is waiting for payment",
268
378
  paid: "is paid and being prepared",
@@ -271,22 +381,35 @@ export function createPickupMcpServer(opts) {
271
381
  expired: "expired before anyone collected it",
272
382
  cancelled: "was cancelled",
273
383
  };
274
- return reply(`Order ${order.id} ${words[order.status] ?? `is ${order.status}`}.`, order);
384
+ const pass = args.claim && (order.status === "paid" || order.status === "ready") ? await passByClaim(shop, order.id, args.claim).catch(() => null) : null;
385
+ 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));
275
386
  }));
276
387
  server.registerTool("get_pickup_pass", {
277
388
  title: "Get the pickup pass",
278
- description: "The pass link, pickup code and QR content for an order this wallet paid, by signing in with the wallet (Sign-In-With-X).",
279
- inputSchema: { order_id: z.string(), ...shopArg },
389
+ description: "The pass link, pickup code and QR for a paid order: with the claim create_pickup_order returned" +
390
+ (opts.payer ? ", or by signing in with the wallet that paid (Sign-In-With-X)." : "."),
391
+ inputSchema: { order_id: z.string(), claim: claimArg, ...shopArg },
280
392
  annotations: { title: "Get the pickup pass", readOnlyHint: true, openWorldHint: true },
393
+ _meta: CARD_TOOL_META,
281
394
  }, guarded(async (args) => {
282
- if (!opts.payer)
283
- throw new ToolError("No wallet is set up here. The pass was emailed to the buyer when the order was paid.");
284
395
  const shop = await shopFor(args.shop_url);
285
- const res = await opts.payer.signIn(`${shop}/api/orders/${encodeURIComponent(args.order_id)}/pass`, doFetch);
286
- if (!res.ok)
287
- throw new ToolError(await errorOf(res, `The shop answered ${res.status}.`));
288
- const pass = (await res.json());
289
- return reply(`Pickup pass: ${pass.passUrl}, pickup code ${pass.pickupCode}.`, pass);
396
+ let pass;
397
+ if (args.claim) {
398
+ pass = await passByClaim(shop, args.order_id, args.claim);
399
+ if (!pass)
400
+ throw new ToolError("This order isn't paid yet, so there's no pass. order_status says where it's at.");
401
+ }
402
+ else if (opts.payer) {
403
+ const res = await opts.payer.signIn(`${shop}/api/orders/${encodeURIComponent(args.order_id)}/pass`, doFetch);
404
+ if (!res.ok)
405
+ throw new ToolError(await errorOf(res, `The shop answered ${res.status}.`));
406
+ pass = (await res.json());
407
+ }
408
+ else {
409
+ 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.");
410
+ }
411
+ await shopName(shop);
412
+ 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);
290
413
  }));
291
414
  const closesIn = (g) => {
292
415
  const minutes = Math.max(0, Math.round((g.closesAt - Date.now()) / 60_000));
package/package.json CHANGED
@@ -1,62 +1,63 @@
1
- {
2
- "name": "@agentpickup/mcp",
3
- "version": "0.3.2",
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
- "viem": "^2.57.14",
60
- "zod": "^4.6.7"
61
- }
62
- }
1
+ {
2
+ "name": "@agentpickup/mcp",
3
+ "version": "0.4.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
+ "qrcode": "^1.5.4",
60
+ "viem": "^2.57.14",
61
+ "zod": "^4.6.7"
62
+ }
63
+ }