@agentpickup/mcp 0.3.3 → 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 +14 -4
- package/dist/card.d.ts +25 -0
- package/dist/card.js +269 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +130 -29
- package/package.json +63 -62
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
|
|
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
|
|
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" } } }
|
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 { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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>✓</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
|
+
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;
|
package/dist/server.js
CHANGED
|
@@ -2,27 +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.
|
|
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
|
|
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.
|
|
14
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
|
|
15
33
|
own x402 wallet. Where the shop takes cards, payment: "card" on
|
|
16
34
|
create_pickup_order gives a checkout link for the user to pay on instead. Where it takes test payments (get_shop says
|
|
17
35
|
so), payment: "test" runs the whole flow with no money moving. Confirm the items and total with the user first.
|
|
18
|
-
4.
|
|
19
|
-
|
|
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.
|
|
20
40
|
Group orders (a coffee run): start_group_order gives a code to share; each person's agent calls join_group_order and
|
|
21
41
|
pays its own share with pay_order; at closing time the shop gets one combined order and the organiser gets its QR.
|
|
22
42
|
group_order_status shows who's in; close_group_order closes it early.`;
|
|
23
43
|
const money = (cents, currency) => new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(cents / 100);
|
|
24
|
-
const reply = (summary, data) => ({
|
|
44
|
+
const reply = (summary, data, card) => ({
|
|
25
45
|
content: [{ type: "text", text: `${summary}\n\n${JSON.stringify(data, null, 2)}` }],
|
|
46
|
+
...(card ? { _meta: { [CARD_META_KEY]: card } } : {}),
|
|
26
47
|
});
|
|
27
48
|
const failure = (message) => ({ content: [{ type: "text", text: message }], isError: true });
|
|
28
49
|
/** 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 +94,44 @@ export function createPickupMcpServer(opts) {
|
|
|
73
94
|
return failure(`Couldn't reach the shop: ${err instanceof Error ? err.message : String(err)}`);
|
|
74
95
|
}
|
|
75
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.");
|
|
76
135
|
const shopArg = opts.allowOtherShops || opts.directoryUrl
|
|
77
136
|
? { shop_url: z.string().optional().describe(`An agentPickup shop's URL, as find_shops returns it. Defaults to ${home}.`) }
|
|
78
137
|
: {};
|
|
@@ -83,6 +142,14 @@ export function createPickupMcpServer(opts) {
|
|
|
83
142
|
websiteUrl: "https://agentpickup.app",
|
|
84
143
|
icons: [{ src: "https://agentpickup.app/logo.png", mimeType: "image/png", sizes: ["1024x1024"] }],
|
|
85
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
|
+
}));
|
|
86
153
|
if (opts.directoryUrl) {
|
|
87
154
|
const directoryUrl = opts.directoryUrl;
|
|
88
155
|
server.registerTool("find_shops", {
|
|
@@ -154,6 +221,7 @@ export function createPickupMcpServer(opts) {
|
|
|
154
221
|
const shop = await shopFor(args.shop_url);
|
|
155
222
|
const data = await call(shop, "/api/catalog");
|
|
156
223
|
const merchant = data.merchant;
|
|
224
|
+
rememberShop(shop, merchant.name);
|
|
157
225
|
const currency = String(data.currency ?? "usd");
|
|
158
226
|
const items = data.items.map((i) => ({
|
|
159
227
|
id: i.id,
|
|
@@ -177,14 +245,14 @@ export function createPickupMcpServer(opts) {
|
|
|
177
245
|
}));
|
|
178
246
|
server.registerTool("create_pickup_order", {
|
|
179
247
|
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.",
|
|
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.",
|
|
183
251
|
inputSchema: {
|
|
184
252
|
items: z
|
|
185
253
|
.array(z.object({ id: z.string().describe("Item id from get_shop"), qty: z.number().int().min(1).max(20) }))
|
|
186
254
|
.min(1),
|
|
187
|
-
email: z.string().email().describe("The user's email. The pickup
|
|
255
|
+
email: z.string().email().describe("The user's email. The pickup pass goes here too."),
|
|
188
256
|
name: z.string().max(100).optional().describe("Name for the order, shown to staff"),
|
|
189
257
|
payment: z
|
|
190
258
|
.enum(["agent", "card", "test"])
|
|
@@ -194,11 +262,14 @@ export function createPickupMcpServer(opts) {
|
|
|
194
262
|
...shopArg,
|
|
195
263
|
},
|
|
196
264
|
annotations: { title: "Order for pickup", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
265
|
+
_meta: CARD_TOOL_META,
|
|
197
266
|
}, guarded(async (args) => {
|
|
198
267
|
const shop = await shopFor(args.shop_url);
|
|
199
268
|
// The agent pays over x402 where the shop takes it, a test shop marks the order paid straight away, and a
|
|
200
269
|
// shop that takes cards gives the user a checkout link.
|
|
201
|
-
const
|
|
270
|
+
const catalog = (await call(shop, "/api/catalog"));
|
|
271
|
+
rememberShop(shop, catalog.merchant?.name);
|
|
272
|
+
const { payments } = catalog;
|
|
202
273
|
const agentRail = payments.x402 ? "x402" : payments.test ? "test" : null;
|
|
203
274
|
const cardRail = payments.square ? "square" : payments.card ? "card" : null;
|
|
204
275
|
const rail = args.payment === "test" ? (payments.test ? "test" : null)
|
|
@@ -217,16 +288,31 @@ export function createPickupMcpServer(opts) {
|
|
|
217
288
|
});
|
|
218
289
|
const order = data.order;
|
|
219
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." : "";
|
|
220
295
|
if (rail === "test") {
|
|
221
|
-
|
|
222
|
-
|
|
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));
|
|
223
310
|
}
|
|
224
311
|
if (rail === "square" || rail === "card") {
|
|
225
|
-
return reply(`Order ${order.id}: ${
|
|
226
|
-
|
|
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 }));
|
|
227
314
|
}
|
|
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 });
|
|
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 }));
|
|
230
316
|
}));
|
|
231
317
|
server.registerTool("pay_order", {
|
|
232
318
|
title: "Pay for an order",
|
|
@@ -279,12 +365,14 @@ export function createPickupMcpServer(opts) {
|
|
|
279
365
|
}));
|
|
280
366
|
server.registerTool("order_status", {
|
|
281
367
|
title: "Check an order",
|
|
282
|
-
description: "Whether an order is waiting for payment, paid, ready to collect, collected or expired."
|
|
283
|
-
|
|
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 },
|
|
284
371
|
annotations: { title: "Check an order", readOnlyHint: true, openWorldHint: true },
|
|
372
|
+
_meta: CARD_TOOL_META,
|
|
285
373
|
}, guarded(async (args) => {
|
|
286
374
|
const shop = await shopFor(args.shop_url);
|
|
287
|
-
const { order } = (await call(shop, `/api/orders/${encodeURIComponent(args.order_id)}`));
|
|
375
|
+
const { order, checkoutUrl } = (await call(shop, `/api/orders/${encodeURIComponent(args.order_id)}`));
|
|
288
376
|
const words = {
|
|
289
377
|
awaiting_payment: "is waiting for payment",
|
|
290
378
|
paid: "is paid and being prepared",
|
|
@@ -293,22 +381,35 @@ export function createPickupMcpServer(opts) {
|
|
|
293
381
|
expired: "expired before anyone collected it",
|
|
294
382
|
cancelled: "was cancelled",
|
|
295
383
|
};
|
|
296
|
-
|
|
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));
|
|
297
386
|
}));
|
|
298
387
|
server.registerTool("get_pickup_pass", {
|
|
299
388
|
title: "Get the pickup pass",
|
|
300
|
-
description: "The pass link, pickup code and QR
|
|
301
|
-
|
|
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 },
|
|
302
392
|
annotations: { title: "Get the pickup pass", readOnlyHint: true, openWorldHint: true },
|
|
393
|
+
_meta: CARD_TOOL_META,
|
|
303
394
|
}, 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
395
|
const shop = await shopFor(args.shop_url);
|
|
307
|
-
|
|
308
|
-
if (
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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);
|
|
312
413
|
}));
|
|
313
414
|
const closesIn = (g) => {
|
|
314
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.
|
|
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.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
|
+
}
|