@agentpickup/mcp 0.4.3 → 0.5.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 +85 -82
- package/dist/card.d.ts +25 -3
- package/dist/card.js +117 -62
- package/dist/server.d.ts +7 -1
- package/dist/server.js +287 -174
- package/dist/share-card.d.ts +5 -4
- package/dist/share-card.js +139 -134
- package/package.json +66 -63
package/README.md
CHANGED
|
@@ -1,82 +1,85 @@
|
|
|
1
|
-
# @agentpickup/mcp
|
|
2
|
-
|
|
3
|
-
An MCP server for [agentPickup](https://agentpickup.app) shops
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
|
12
|
-
|
|
13
|
-
| `
|
|
14
|
-
| `
|
|
15
|
-
| `
|
|
16
|
-
| `
|
|
17
|
-
| `
|
|
18
|
-
| `
|
|
19
|
-
| `
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
(`
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
|
64
|
-
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
1
|
+
# @agentpickup/mcp
|
|
2
|
+
|
|
3
|
+
An MCP server for [agentPickup](https://agentpickup.app): people order from real shops through their AI assistant and
|
|
4
|
+
collect in person. It lets the assistant find shops near its user, see what they sell, answer questions the menu
|
|
5
|
+
doesn't (dietary needs, allergies, hours, parking), order, and have the user pay by card on the shop's own checkout
|
|
6
|
+
(or pay in USDC over x402 from an agent wallet). The order lands on the shop's own POS, and the user collects with the
|
|
7
|
+
QR they show at the counter.
|
|
8
|
+
|
|
9
|
+
## Tools
|
|
10
|
+
|
|
11
|
+
| Tool | What it does |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `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 |
|
|
14
|
+
| `get_shop` | Items and prices, the pickup location, whether it's open, its social accounts, and how the shop takes payment |
|
|
15
|
+
| `ask_about_shop` | Questions the menu doesn't answer: dietary needs and allergies, reviews, opening hours, where it is, parking, wait times, prices. Answers from what the shop has said, and says plainly what it hasn't. Send the question without names or personal details: the shop sees only which topics people ask about |
|
|
16
|
+
| `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 |
|
|
17
|
+
| `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 |
|
|
18
|
+
| `order_status` | Waiting for payment, paid, ready, collected or expired. With the order's `claim`, a paid order's pass comes back too |
|
|
19
|
+
| `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) |
|
|
20
|
+
| `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 |
|
|
21
|
+
| `join_group_order` | Adds the user's items to a group order by its code; pay the share with `pay_order` before it closes |
|
|
22
|
+
| `group_order_status` | Who's in, what they ordered, who has paid, and whether the shop's order is ready or collected |
|
|
23
|
+
| `share_agentpickup` | A QR and link for friends to add agentPickup to their assistant (as a card where the chat shows cards), for everyone or with one shop picked |
|
|
24
|
+
| `close_group_order` | Closes a group order early, as its organiser; the shop gets one combined order and the organiser its QR |
|
|
25
|
+
|
|
26
|
+
## The pickup pass card
|
|
27
|
+
|
|
28
|
+
In chat apps that draw [MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview) cards (Claude and ChatGPT
|
|
29
|
+
among them), `create_pickup_order`, `order_status` and `get_pickup_pass` show the order as a card: a Pay button while
|
|
30
|
+
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
|
|
31
|
+
tile turns lime. The card is one self-contained page (`ui://agentpickup/pass-<version>.html`) that loads nothing from
|
|
32
|
+
anywhere else. In apps without cards, the agent gives the user the pass link and pickup code. The shop emails the pass
|
|
33
|
+
either way. The card's "Invite a friend" link, and `share_agentpickup`'s own card, point at the add page
|
|
34
|
+
(`addPageUrl`, default https://agentpickup.app/add) so friends can add it too.
|
|
35
|
+
|
|
36
|
+
## Use it
|
|
37
|
+
|
|
38
|
+
Try it against the hosted demo shops first. The hosted server holds no wallet: its demo shops take a free test
|
|
39
|
+
payment, the Square sandbox's test card, or USDC from the user's own x402 wallet:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{ "mcpServers": { "agentpickup": { "type": "http", "url": "https://agentpickup.app/mcp" } } }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Or run it locally against any agentPickup shop, with your own wallet:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"agentpickup": {
|
|
51
|
+
"command": "npx",
|
|
52
|
+
"args": ["-y", "@agentpickup/mcp"],
|
|
53
|
+
"env": {
|
|
54
|
+
"AGENTPICKUP_SHOP_URL": "https://orders.yourshop.com",
|
|
55
|
+
"AGENTPICKUP_WALLET_KEY": "0x…",
|
|
56
|
+
"AGENTPICKUP_MAX_PER_ORDER": "$25"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
| Setting | Default | |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `AGENTPICKUP_SHOP_URL` | the demo shop | The shop to order from. Tool calls can also name another shop with `shop_url` |
|
|
66
|
+
| `AGENTPICKUP_WALLET_KEY` | none | An EVM private key holding USDC. Without it, `pay_order` only quotes |
|
|
67
|
+
| `AGENTPICKUP_MAX_PER_ORDER` | `$25` | The most the wallet pays for one order |
|
|
68
|
+
| `AGENTPICKUP_NETWORKS` | `eip155:84532` | Networks the wallet may pay on. Base Sepolia (test USDC) unless you add `eip155:8453` for Base mainnet |
|
|
69
|
+
| `AGENTPICKUP_DIRECTORY_URL` | `https://agentpickup.app/directory/v1/shops` | Where `find_shops` searches. `off` leaves the tool out |
|
|
70
|
+
|
|
71
|
+
Use a wallet that holds only what you're willing to let an agent spend. The tools tell the model to confirm
|
|
72
|
+
items and the total with the user before paying.
|
|
73
|
+
|
|
74
|
+
## Host it in your own server
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { createMcpHandler, walletPayer } from "@agentpickup/mcp";
|
|
78
|
+
|
|
79
|
+
const mcp = createMcpHandler({ shopUrl: "https://orders.yourshop.com" }); // no wallet: pay_order quotes
|
|
80
|
+
// `instructions` adds your own notes to what agents read on connecting, e.g. which of your shops take what.
|
|
81
|
+
app.all("/mcp", (c) => mcp(c.req.raw)); // Streamable HTTP, stateless, any Request/Response runtime
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Node 22.13 or later. Licence: FSL-1.1-ALv2 (free to use and self-host; becomes Apache-2.0 two years after
|
|
85
|
+
each release).
|
package/dist/card.d.ts
CHANGED
|
@@ -1,27 +1,49 @@
|
|
|
1
1
|
export declare const CARD_MIME_TYPE = "text/html;profile=mcp-app";
|
|
2
|
-
/**
|
|
2
|
+
/** The _meta key that mirrors structuredContent.card, for hosts that pass _meta. */
|
|
3
3
|
export declare const CARD_META_KEY = "agentpickup/pass";
|
|
4
|
+
/** A QR as a bit grid: size × size modules, row by row, packed 8 to a byte (first module in the top bit), base64. */
|
|
5
|
+
export interface QrBits {
|
|
6
|
+
size: number;
|
|
7
|
+
bits: string;
|
|
8
|
+
}
|
|
4
9
|
/** What the card draws. Every field but orderId is optional: later results fill in or update what's there. */
|
|
5
10
|
export interface PassCard {
|
|
6
11
|
v: 1;
|
|
7
12
|
orderId: string;
|
|
8
13
|
shopUrl?: string;
|
|
9
14
|
shopName?: string;
|
|
15
|
+
/** Where to collect: the shop's address, or its counter's name. */
|
|
16
|
+
where?: string;
|
|
10
17
|
/** Lets the card call order_status itself while the order moves along. */
|
|
11
18
|
claim?: string;
|
|
12
19
|
status?: string;
|
|
20
|
+
/** A paid order staff have started. */
|
|
21
|
+
accepted?: boolean;
|
|
13
22
|
rail?: string;
|
|
23
|
+
/** "2× Flat white, 1× Croissant" */
|
|
14
24
|
items?: string;
|
|
25
|
+
/** The order's lines with their prices, for the card's receipt while it's unpaid. */
|
|
26
|
+
lines?: {
|
|
27
|
+
qty: number;
|
|
28
|
+
name: string;
|
|
29
|
+
price?: string;
|
|
30
|
+
}[];
|
|
15
31
|
total?: string;
|
|
16
32
|
collectBy?: number | null;
|
|
17
33
|
collectedAt?: number | null;
|
|
18
34
|
checkoutUrl?: string;
|
|
19
35
|
passUrl?: string;
|
|
20
36
|
pickupCode?: string | null;
|
|
21
|
-
/** The QR
|
|
22
|
-
|
|
37
|
+
/** The pass's QR, once it's paid. */
|
|
38
|
+
qr?: QrBits;
|
|
23
39
|
/** The add page's share mode, for "Invite a friend". */
|
|
24
40
|
inviteUrl?: string;
|
|
25
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Draws a QR bit grid as an SVG path, one run of dark modules per segment. Plain JavaScript source, so the cards
|
|
44
|
+
* run exactly this text; the server and tests use the same function below.
|
|
45
|
+
*/
|
|
46
|
+
export declare const QR_PATH_JS = "function (qr, decode) {\n var raw = (decode || function (b) { return atob(b); })(qr.bits);\n var size = qr.size;\n function dark(i) { return (raw.charCodeAt(i >> 3) >> (7 - (i & 7))) & 1; }\n var d = \"\";\n for (var y = 0; y < size; y++) {\n var x = 0;\n while (x < size) {\n if (!dark(y * size + x)) { x++; continue; }\n var start = x;\n while (x < size && dark(y * size + x)) x++;\n d += \"M\" + start + \" \" + y + \"h\" + (x - start) + \"v1h-\" + (x - start) + \"z\";\n }\n }\n return d;\n}";
|
|
47
|
+
export declare const qrPathFromBits: (qr: QrBits, decode?: (b64: string) => string) => string;
|
|
26
48
|
/** The card's page. Its version goes in the handshake, so a host's logs say which card it ran. */
|
|
27
49
|
export declare function passCardHtml(version: string): string;
|
package/dist/card.js
CHANGED
|
@@ -1,14 +1,40 @@
|
|
|
1
1
|
// The pickup pass as an MCP Apps card (ui:// resource, text/html;profile=mcp-app): chat apps that show cards (Claude,
|
|
2
|
-
// ChatGPT) draw it beside the tool result.
|
|
3
|
-
//
|
|
4
|
-
// tile turns brand lime. One self-contained page: no scripts,
|
|
2
|
+
// ChatGPT) draw it beside the tool result. While the order is unpaid it shows what's in it and a Pay button; once
|
|
3
|
+
// paid, the QR, pickup code and collect-by time; and it keeps itself up to date (order_status with the order's
|
|
4
|
+
// claim) until it's collected. At "ready" the QR's tile turns brand lime. One self-contained page: no scripts,
|
|
5
|
+
// fonts or images from anywhere else.
|
|
6
|
+
//
|
|
7
|
+
// The card reads its data from the tool result's structuredContent.card. Hosts pass structuredContent to the card
|
|
8
|
+
// (Claude drops _meta), so everything the card draws goes there, with the QR as a compact bit grid rather than an
|
|
9
|
+
// image. _meta carries the same for hosts that read it, and the text JSON is a last resort.
|
|
5
10
|
//
|
|
6
11
|
// The bridge is the MCP Apps postMessage protocol (JSON-RPC 2.0 with the host, protocol 2026-01-26), written out
|
|
7
12
|
// here rather than bundling the 300 KB SDK: ui/initialize, ui/notifications/initialized, tool-result,
|
|
8
13
|
// host-context-changed, resource-teardown, tools/call, ui/open-link and ui/notifications/size-changed.
|
|
9
14
|
export const CARD_MIME_TYPE = "text/html;profile=mcp-app";
|
|
10
|
-
/**
|
|
15
|
+
/** The _meta key that mirrors structuredContent.card, for hosts that pass _meta. */
|
|
11
16
|
export const CARD_META_KEY = "agentpickup/pass";
|
|
17
|
+
/**
|
|
18
|
+
* Draws a QR bit grid as an SVG path, one run of dark modules per segment. Plain JavaScript source, so the cards
|
|
19
|
+
* run exactly this text; the server and tests use the same function below.
|
|
20
|
+
*/
|
|
21
|
+
export const QR_PATH_JS = `function (qr, decode) {
|
|
22
|
+
var raw = (decode || function (b) { return atob(b); })(qr.bits);
|
|
23
|
+
var size = qr.size;
|
|
24
|
+
function dark(i) { return (raw.charCodeAt(i >> 3) >> (7 - (i & 7))) & 1; }
|
|
25
|
+
var d = "";
|
|
26
|
+
for (var y = 0; y < size; y++) {
|
|
27
|
+
var x = 0;
|
|
28
|
+
while (x < size) {
|
|
29
|
+
if (!dark(y * size + x)) { x++; continue; }
|
|
30
|
+
var start = x;
|
|
31
|
+
while (x < size && dark(y * size + x)) x++;
|
|
32
|
+
d += "M" + start + " " + y + "h" + (x - start) + "v1h-" + (x - start) + "z";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return d;
|
|
36
|
+
}`;
|
|
37
|
+
export const qrPathFromBits = new Function(`return ${QR_PATH_JS}`)();
|
|
12
38
|
const PAGE = String.raw `<!doctype html>
|
|
13
39
|
<html lang="en">
|
|
14
40
|
<head>
|
|
@@ -16,46 +42,53 @@ const PAGE = String.raw `<!doctype html>
|
|
|
16
42
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
17
43
|
<title>Pickup pass</title>
|
|
18
44
|
<style>
|
|
19
|
-
:root{--bg:#ffffff;--surface:#f4f4f1;--text:#16161a;--muted:#66666d;--line:rgba(10,11,13,.
|
|
20
|
-
:root[data-theme=dark]{--bg:#1d1d20;--surface:#
|
|
21
|
-
@media (prefers-color-scheme:dark){:root:not([data-theme]){--bg:#1d1d20;--surface:#
|
|
45
|
+
:root{--bg:#ffffff;--surface:#f4f4f1;--text:#16161a;--muted:#66666d;--faint:#9a9aa0;--line:rgba(10,11,13,.1);--ok:#23843b;--okbg:#e8f5ea;--warn:#8a5a00;--warnbg:#fdf2dc;--lime:#b5f23d;--ink:#0a0b0d;color-scheme:light}
|
|
46
|
+
:root[data-theme=dark]{--bg:#1d1d20;--surface:#28282c;--text:#f2f2f4;--muted:#a3a3ab;--faint:#74747c;--line:rgba(255,255,255,.12);--ok:#8fd47f;--okbg:#1f3322;--warn:#f2c46b;--warnbg:#3a2f17;color-scheme:dark}
|
|
47
|
+
@media (prefers-color-scheme:dark){:root:not([data-theme]){--bg:#1d1d20;--surface:#28282c;--text:#f2f2f4;--muted:#a3a3ab;--faint:#74747c;--line:rgba(255,255,255,.12);--ok:#8fd47f;--okbg:#1f3322;--warn:#f2c46b;--warnbg:#3a2f17;color-scheme:dark}}
|
|
22
48
|
html,body{margin:0;padding:0;background:transparent}
|
|
23
49
|
body{font-family:var(--font-sans,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif);color:var(--text);-webkit-font-smoothing:antialiased;padding:2px}
|
|
24
|
-
.card{box-sizing:border-box;max-width:380px;margin:0 auto;background:var(--bg);border:1px solid var(--line);border-radius:
|
|
25
|
-
.head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px
|
|
26
|
-
.shop{font-weight:
|
|
27
|
-
.
|
|
28
|
-
.
|
|
29
|
-
.
|
|
30
|
-
.
|
|
31
|
-
.
|
|
32
|
-
.
|
|
50
|
+
.card{box-sizing:border-box;max-width:380px;margin:0 auto;background:var(--bg);border:1px solid var(--line);border-radius:18px;padding:16px 18px 14px}
|
|
51
|
+
.head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px}
|
|
52
|
+
.shop{font-weight:650;font-size:16px;margin:0;letter-spacing:-.01em}
|
|
53
|
+
.sub{font-size:13px;color:var(--muted);margin:2px 0 0}
|
|
54
|
+
.pill{font-size:12px;line-height:1;padding:6px 10px;border-radius:999px;white-space:nowrap;background:var(--surface);color:var(--muted)}
|
|
55
|
+
.pill.awaiting_payment{background:var(--warnbg);color:var(--warn)}
|
|
56
|
+
.pill.paid,.pill.collected{background:var(--okbg);color:var(--ok)}
|
|
57
|
+
.pill.ready{background:var(--lime);color:var(--ink);font-weight:650}
|
|
58
|
+
.lines{margin:14px 0 0;border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:8px 0;font-size:14px}
|
|
59
|
+
.line{display:flex;justify-content:space-between;gap:12px;padding:3px 0}
|
|
60
|
+
.line span:first-child{min-width:0;overflow-wrap:anywhere}
|
|
61
|
+
.line.total{font-weight:650}
|
|
62
|
+
.wrap{position:relative;border-radius:14px;margin-top:14px}
|
|
63
|
+
.tile{position:relative;overflow:hidden;background:#ffffff;border-radius:14px;padding:16px;display:flex;justify-content:center;align-items:center;border:1px solid var(--line);min-height:120px}
|
|
64
|
+
.tile svg{position:relative;display:block;width:100%;max-width:210px;height:auto}
|
|
33
65
|
.pour{position:absolute;left:0;right:0;bottom:0;height:0;background:var(--lime);transition:height 1s cubic-bezier(.5,0,.2,1)}
|
|
34
66
|
.wrap.ready .pour{height:100%}
|
|
35
67
|
.wrap.ready .tile{border-color:var(--lime)}
|
|
36
|
-
.wrap.ready::after{content:"";position:absolute;inset:0;border:2px solid var(--lime);border-radius:
|
|
68
|
+
.wrap.ready::after{content:"";position:absolute;inset:0;border:2px solid var(--lime);border-radius:14px;animation:ring 1.8s ease-out infinite;pointer-events:none}
|
|
37
69
|
@keyframes ring{from{transform:scale(1);opacity:.9}to{transform:scale(1.08);opacity:0}}
|
|
38
70
|
.tile.plain{background:var(--surface)}
|
|
39
71
|
.done{font-size:15px;color:var(--text);text-align:center}
|
|
40
|
-
.done b{display:block;font-size:
|
|
41
|
-
.facts{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:
|
|
42
|
-
.fact{background:var(--surface);border-radius:
|
|
72
|
+
.done b{display:block;font-size:30px;line-height:1.2;color:var(--ok)}
|
|
73
|
+
.facts{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:8px;margin:10px 0 0}
|
|
74
|
+
.fact{background:var(--surface);border-radius:12px;padding:8px 12px}
|
|
43
75
|
.fact span{display:block;font-size:12px;color:var(--muted)}
|
|
44
|
-
.fact b{display:block;font-size:
|
|
76
|
+
.fact b{display:block;font-size:19px;font-weight:650;overflow-wrap:anywhere}
|
|
45
77
|
.code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;letter-spacing:2px}
|
|
46
|
-
.steps{display:flex;
|
|
47
|
-
.step{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--
|
|
48
|
-
.
|
|
49
|
-
.step.past{color:var(--ok)}.step.past
|
|
50
|
-
.step.now{color:var(--text);font-weight:
|
|
78
|
+
.steps{display:flex;gap:6px;margin-top:14px}
|
|
79
|
+
.step{flex:1;display:flex;align-items:center;gap:6px;font-size:12px;color:var(--faint)}
|
|
80
|
+
.step i{width:9px;height:9px;border-radius:50%;border:1.5px solid currentColor;box-sizing:border-box;flex:none}
|
|
81
|
+
.step.past{color:var(--ok)}.step.past i{background:currentColor}
|
|
82
|
+
.step.now{color:var(--text);font-weight:650}.step.now i{background:var(--lime);border-color:var(--lime)}
|
|
51
83
|
.msg{font-size:13px;color:var(--muted);margin:10px 0 0;line-height:1.45}
|
|
52
84
|
.test{font-size:12px;color:var(--muted);margin:6px 0 0}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
.
|
|
58
|
-
.foot
|
|
85
|
+
button{font:inherit;cursor:pointer}
|
|
86
|
+
.pay{display:block;width:100%;margin:14px 0 0;padding:13px 14px;border-radius:12px;border:0;background:var(--lime);color:var(--ink);font-size:15px;font-weight:650}
|
|
87
|
+
.note{font-size:12px;color:var(--muted);margin:8px 0 0;text-align:center;line-height:1.4}
|
|
88
|
+
.link{background:none;border:none;padding:0;color:var(--muted);text-decoration:underline;font-size:13px}
|
|
89
|
+
.actions{margin-top:10px}
|
|
90
|
+
.foot{font-size:11px;color:var(--faint);margin-top:14px;display:flex;justify-content:space-between;align-items:center;gap:8px}
|
|
91
|
+
.foot .link{font-size:12px;color:var(--muted)}
|
|
59
92
|
@media (prefers-reduced-motion:reduce){.pour{transition:none}.wrap.ready::after{animation:none;opacity:1}}
|
|
60
93
|
</style>
|
|
61
94
|
</head>
|
|
@@ -69,6 +102,7 @@ button.link{background:none;border:none;padding:0;color:var(--muted);text-decora
|
|
|
69
102
|
var root = document.getElementById("root");
|
|
70
103
|
var state = null, canCall = false, canOpen = false, locale, timeZone;
|
|
71
104
|
var nextId = 1, pending = {}, timer = null, started = Date.now(), lastHtml = "", poured = false, lastHeight = -1, stopped = false;
|
|
105
|
+
var qrPathFromBits = __QR_PATH__;
|
|
72
106
|
|
|
73
107
|
// ---------- the bridge ----------
|
|
74
108
|
var host = window.parent;
|
|
@@ -102,7 +136,7 @@ button.link{background:none;border:none;padding:0;color:var(--muted);text-decora
|
|
|
102
136
|
function applyContext(ctx) {
|
|
103
137
|
if (ctx.theme === "light" || ctx.theme === "dark") document.documentElement.setAttribute("data-theme", ctx.theme);
|
|
104
138
|
var vars = ctx.styles && ctx.styles.variables;
|
|
105
|
-
if (vars
|
|
139
|
+
if (vars && typeof vars["--font-sans"] === "string") document.documentElement.style.setProperty("--font-sans", vars["--font-sans"]);
|
|
106
140
|
if (ctx.locale) locale = ctx.locale;
|
|
107
141
|
if (ctx.timeZone) timeZone = ctx.timeZone;
|
|
108
142
|
}
|
|
@@ -115,15 +149,17 @@ button.link{background:none;border:none;padding:0;color:var(--muted);text-decora
|
|
|
115
149
|
// ---------- results ----------
|
|
116
150
|
function cardOf(result) {
|
|
117
151
|
if (!result) return null;
|
|
152
|
+
var sc = result.structuredContent && result.structuredContent.card;
|
|
153
|
+
if (sc && sc.orderId) return sc;
|
|
118
154
|
var meta = result._meta && result._meta["agentpickup/pass"];
|
|
119
155
|
if (meta && meta.orderId) return meta;
|
|
120
|
-
//
|
|
156
|
+
// Neither came through: the JSON after the tool's summary line still has the basics.
|
|
121
157
|
try {
|
|
122
158
|
var text = result.content && result.content[0] && result.content[0].text;
|
|
123
159
|
var data = JSON.parse(text.slice(text.indexOf("\n\n") + 2));
|
|
124
160
|
var id = data.orderId || data.id || (data.order && data.order.id);
|
|
125
161
|
if (!id) return null;
|
|
126
|
-
return { orderId: id, status: data.status, passUrl: data.passUrl, pickupCode: data.pickupCode, checkoutUrl: data.checkoutUrl, total: data.total, claim: data.claim, shopUrl: data.shop };
|
|
162
|
+
return { orderId: id, status: data.status || (data.checkoutUrl || data.payUrl ? "awaiting_payment" : undefined), passUrl: data.passUrl, pickupCode: data.pickupCode, checkoutUrl: data.checkoutUrl, total: data.total, claim: data.claim, shopUrl: data.shop };
|
|
127
163
|
} catch (e) { return null; }
|
|
128
164
|
}
|
|
129
165
|
function onResult(result) {
|
|
@@ -172,36 +208,59 @@ button.link{background:none;border:none;padding:0;color:var(--muted);text-decora
|
|
|
172
208
|
return s;
|
|
173
209
|
} catch (e) { return d.toLocaleTimeString(); }
|
|
174
210
|
}
|
|
175
|
-
function
|
|
211
|
+
function qrSvg(qr) {
|
|
212
|
+
if (!qr || typeof qr.bits !== "string" || !(qr.size > 0 && qr.size < 200)) return "";
|
|
213
|
+
try {
|
|
214
|
+
return '<svg viewBox="0 0 ' + qr.size + " " + qr.size + '" shape-rendering="crispEdges" role="img" aria-label="Pickup QR code"><path fill="#0a0b0d" d="' + qrPathFromBits(qr) + '"/></svg>';
|
|
215
|
+
} catch (e) { return ""; }
|
|
216
|
+
}
|
|
176
217
|
function message(s) {
|
|
177
218
|
var live = canCall && s.claim;
|
|
178
219
|
switch (s.status) {
|
|
179
|
-
case "awaiting_payment":
|
|
180
|
-
|
|
181
|
-
? "Pay on the checkout page. " + (live ? "This card turns into your pickup pass once it's paid." : "Then ask for the pickup pass.")
|
|
182
|
-
: "Waiting for the payment. " + (live ? "This card turns into your pickup pass once it's paid." : "");
|
|
183
|
-
case "paid": return "Paid. " + (live ? "This turns green when it's ready. " : "") + "The pass is in your email too.";
|
|
220
|
+
case "awaiting_payment": return s.checkoutUrl ? "" : "Waiting for the payment. " + (live ? "This card becomes your pickup pass once it's paid." : "");
|
|
221
|
+
case "paid": return (s.accepted ? "Paid. The shop is making it. " : "Paid. The shop has your order. ") + (live ? "This turns green when it's ready. " : "") + "The pass is in your email too.";
|
|
184
222
|
case "ready": return "Ready at the counter. Show the QR or say the code.";
|
|
185
|
-
case "collected": return
|
|
223
|
+
case "collected": return "Enjoy. The receipt is in your email.";
|
|
186
224
|
case "expired": return "This order expired before anyone collected it.";
|
|
187
225
|
case "cancelled": return "This order was cancelled.";
|
|
188
226
|
default: return "";
|
|
189
227
|
}
|
|
190
228
|
}
|
|
229
|
+
function steps(status) {
|
|
230
|
+
var first = status === "awaiting_payment" ? "Pay" : "Paid";
|
|
231
|
+
var at = { awaiting_payment: 0, paid: 1, ready: 1, collected: 3 }[status];
|
|
232
|
+
if (at === undefined) return "";
|
|
233
|
+
return '<div class="steps">' + [first, "Ready", "Collected"].map(function (name, i) {
|
|
234
|
+
var cls = i < at || (status === "ready" && i === 1) ? " past" : i === at ? " now" : "";
|
|
235
|
+
if (status === "ready" && i === 2) cls = " now";
|
|
236
|
+
return '<span class="step' + cls + '"><i></i>' + name + "</span>";
|
|
237
|
+
}).join("") + "</div>";
|
|
238
|
+
}
|
|
191
239
|
function view(s) {
|
|
192
240
|
var status = s.status || "awaiting_payment";
|
|
193
|
-
var
|
|
241
|
+
var unpaid = status === "awaiting_payment";
|
|
194
242
|
var live = status === "paid" || status === "ready";
|
|
195
|
-
var h = '<div class="card">';
|
|
196
|
-
h += '<
|
|
197
|
-
|
|
198
|
-
h += '
|
|
199
|
-
|
|
243
|
+
var h = '<div class="card"><div class="head"><div>';
|
|
244
|
+
h += '<p class="shop">' + esc(s.shopName || "Your pickup order") + "</p>";
|
|
245
|
+
var sub = unpaid ? (s.where ? "Pickup at " + s.where : "") : (s.items ? s.items + (s.total ? " · " + s.total : "") : "");
|
|
246
|
+
if (sub) h += '<p class="sub">' + esc(sub) + "</p>";
|
|
247
|
+
h += '</div><span class="pill ' + esc(status) + '">' + esc(LABELS[status] || status) + "</span></div>";
|
|
248
|
+
|
|
249
|
+
if (unpaid) {
|
|
250
|
+
var lines = s.lines && s.lines.length ? s.lines : null;
|
|
251
|
+
if (lines || s.total) {
|
|
252
|
+
h += '<div class="lines">';
|
|
253
|
+
if (lines) lines.forEach(function (l) { h += '<div class="line"><span>' + esc(l.qty + "× " + l.name) + "</span><span>" + esc(l.price || "") + "</span></div>"; });
|
|
254
|
+
else if (s.items) h += '<div class="line"><span>' + esc(s.items) + "</span><span></span></div>";
|
|
255
|
+
if (s.total) h += '<div class="line total"><span>Total</span><span>' + esc(s.total) + "</span></div>";
|
|
256
|
+
h += "</div>";
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
var svg = qrSvg(s.qr);
|
|
200
260
|
if (live && svg) {
|
|
201
|
-
h += '<div class="wrap' + (status === "ready" && poured ? " ready" : "") + '" id="wrap"><div class="tile"><div class="pour"></div>' +
|
|
202
|
-
svg.replace("<svg", '<svg role="img" aria-label="Pickup QR code"') + "</div></div>";
|
|
261
|
+
h += '<div class="wrap' + (status === "ready" && poured ? " ready" : "") + '" id="wrap"><div class="tile"><div class="pour"></div>' + svg + "</div></div>";
|
|
203
262
|
} else if (status === "collected") {
|
|
204
|
-
h += '<div class="wrap"><div class="tile plain"><div class="done"><b>✓</b>Collected</div></div></div>
|
|
263
|
+
h += '<div class="wrap"><div class="tile plain"><div class="done"><b>✓</b>' + esc(s.collectedAt ? "Collected at " + time(s.collectedAt) : "Collected") + "</div></div></div>";
|
|
205
264
|
}
|
|
206
265
|
if (live && (s.pickupCode || s.collectBy)) {
|
|
207
266
|
h += '<div class="facts">';
|
|
@@ -209,19 +268,15 @@ button.link{background:none;border:none;padding:0;color:var(--muted);text-decora
|
|
|
209
268
|
if (s.collectBy) h += '<div class="fact"><span>Collect by</span><b>' + esc(time(s.collectBy)) + "</b></div>";
|
|
210
269
|
h += "</div>";
|
|
211
270
|
}
|
|
212
|
-
|
|
213
|
-
var at = { paid: 0, ready: 1, collected: 2 }[status];
|
|
214
|
-
h += '<div class="steps">' + ["Paid", "Ready", "Collected"].map(function (name, i) {
|
|
215
|
-
return '<span class="step' + (i < at ? " past" : i === at ? " now" : "") + '"><span class="dot"></span>' + name + "</span>";
|
|
216
|
-
}).join("") + "</div>";
|
|
217
|
-
}
|
|
271
|
+
h += steps(status);
|
|
218
272
|
var msg = message(s);
|
|
219
273
|
if (msg) h += '<p class="msg">' + esc(msg) + "</p>";
|
|
274
|
+
if (unpaid && s.checkoutUrl) {
|
|
275
|
+
h += '<button class="pay" data-open="checkoutUrl">Pay' + (s.total ? " " + esc(s.total) : "") + "</button>";
|
|
276
|
+
h += '<p class="note">Card, Apple Pay or Google Pay on the shop\'s secure Square checkout. This card becomes your pickup pass.</p>';
|
|
277
|
+
}
|
|
220
278
|
if (s.rail === "test") h += '<p class="test">Test order: no money moved.</p>';
|
|
221
|
-
|
|
222
|
-
if (status === "awaiting_payment" && s.checkoutUrl) actions += '<button class="pay" data-open="checkoutUrl">Pay' + (s.total ? " " + esc(s.total) : "") + " by card</button>";
|
|
223
|
-
if (live && s.passUrl) actions += '<button class="link" data-open="passUrl">Open the pass</button>';
|
|
224
|
-
if (actions) h += '<div class="row">' + actions + "</div>";
|
|
279
|
+
if (live && s.passUrl) h += '<div class="actions"><button class="link" data-open="passUrl">Open the pass</button></div>';
|
|
225
280
|
h += '<div class="foot">' + (s.inviteUrl ? '<button class="link" data-open="inviteUrl">Invite a friend</button>' : "<span></span>") + "<span>agentPickup</span></div></div>";
|
|
226
281
|
return h;
|
|
227
282
|
}
|
|
@@ -266,5 +321,5 @@ button.link{background:none;border:none;padding:0;color:var(--muted);text-decora
|
|
|
266
321
|
</html>`;
|
|
267
322
|
/** The card's page. Its version goes in the handshake, so a host's logs say which card it ran. */
|
|
268
323
|
export function passCardHtml(version) {
|
|
269
|
-
return PAGE.replace("__VERSION__", version);
|
|
324
|
+
return PAGE.replace("__VERSION__", version).replace("__QR_PATH__", QR_PATH_JS);
|
|
270
325
|
}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { type Fetch, type Payer } from "./wallet.js";
|
|
3
|
-
export declare const VERSION = "0.
|
|
3
|
+
export declare const VERSION = "0.5.0";
|
|
4
4
|
export interface PickupMcpOptions {
|
|
5
5
|
/** The agentPickup shop this server orders from. */
|
|
6
6
|
shopUrl: string;
|
|
@@ -22,6 +22,12 @@ export interface PickupMcpOptions {
|
|
|
22
22
|
payer?: Payer;
|
|
23
23
|
/** Notes added to the instructions agents read on connecting, e.g. which of this host's shops take what. */
|
|
24
24
|
instructions?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Card and test payments only, for connector directories that don't take crypto (Claude's, ChatGPT's): no
|
|
27
|
+
* pay_order, no group orders (their shares pay in USDC), and nothing about USDC or wallets. The person pays on
|
|
28
|
+
* the shop's own card checkout, so the connector itself never moves money.
|
|
29
|
+
*/
|
|
30
|
+
cardsOnly?: boolean;
|
|
25
31
|
/**
|
|
26
32
|
* The page where people add agentPickup to their assistant, which share_agentpickup and the pass card's
|
|
27
33
|
* "Invite a friend" link point to. Default https://agentpickup.app/add.
|