@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 CHANGED
@@ -1,82 +1,85 @@
1
- # @agentpickup/mcp
2
-
3
- An MCP server for [agentPickup](https://agentpickup.app) shops. It lets an AI agent find
4
- pickup shops near its user, see what they sell, order, pay by card or in USDC over x402, and hand its user the
5
- QR they show at the counter to collect.
6
-
7
- ## Tools
8
-
9
- | Tool | What it does |
10
- |---|---|
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
- | `get_shop` | Items and prices, the pickup location, and how the shop takes payment |
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
- | `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. 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
- | `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
- | `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
- | `group_order_status` | Who's in, what they ordered, who has paid, and whether the shop's order is ready or collected |
20
- | `share_agentpickup` | A QR and link for friends to add agentPickup to their assistant (as a card where the chat shows cards), for everyone or with one café picked |
21
- | `close_group_order` | Closes a group order early, as its organiser; the shop gets one combined order and the organiser its QR |
22
-
23
- ## The pickup pass card
24
-
25
- In chat apps that draw [MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview) cards (Claude and ChatGPT
26
- among them), `create_pickup_order`, `order_status` and `get_pickup_pass` show the order as a card: a Pay button while
27
- it's unpaid, then the QR and pickup code, kept up to date until it's collected. When the shop marks it ready, the QR's
28
- tile turns lime. The card is one self-contained page (`ui://agentpickup/pass-<version>.html`) that loads nothing from
29
- anywhere else. In apps without cards, the agent gives the user the pass link and pickup code. The shop emails the pass
30
- either way. The card's "Invite a friend" link, and `share_agentpickup`'s own card, point at the add page
31
- (`addPageUrl`, default https://agentpickup.app/add) so friends can add it too.
32
-
33
- ## Use it
34
-
35
- Try it against the hosted demo shops first. The hosted server holds no wallet: its demo shops take a free test
36
- payment, the Square sandbox's test card, or USDC from the user's own x402 wallet:
37
-
38
- ```json
39
- { "mcpServers": { "agentpickup": { "type": "http", "url": "https://agentpickup.app/mcp" } } }
40
- ```
41
-
42
- Or run it locally against any agentPickup shop, with your own wallet:
43
-
44
- ```json
45
- {
46
- "mcpServers": {
47
- "agentpickup": {
48
- "command": "npx",
49
- "args": ["-y", "@agentpickup/mcp"],
50
- "env": {
51
- "AGENTPICKUP_SHOP_URL": "https://orders.yourshop.com",
52
- "AGENTPICKUP_WALLET_KEY": "0x…",
53
- "AGENTPICKUP_MAX_PER_ORDER": "$25"
54
- }
55
- }
56
- }
57
- }
58
- ```
59
-
60
- | Setting | Default | |
61
- |---|---|---|
62
- | `AGENTPICKUP_SHOP_URL` | the demo shop | The shop to order from. Tool calls can also name another shop with `shop_url` |
63
- | `AGENTPICKUP_WALLET_KEY` | none | An EVM private key holding USDC. Without it, `pay_order` only quotes |
64
- | `AGENTPICKUP_MAX_PER_ORDER` | `$25` | The most the wallet pays for one order |
65
- | `AGENTPICKUP_NETWORKS` | `eip155:84532` | Networks the wallet may pay on. Base Sepolia (test USDC) unless you add `eip155:8453` for Base mainnet |
66
- | `AGENTPICKUP_DIRECTORY_URL` | `https://agentpickup.app/directory/v1/shops` | Where `find_shops` searches. `off` leaves the tool out |
67
-
68
- Use a wallet that holds only what you're willing to let an agent spend. The tools tell the model to confirm
69
- items and the total with the user before paying.
70
-
71
- ## Host it in your own server
72
-
73
- ```ts
74
- import { createMcpHandler, walletPayer } from "@agentpickup/mcp";
75
-
76
- const mcp = createMcpHandler({ shopUrl: "https://orders.yourshop.com" }); // no wallet: pay_order quotes
77
- // `instructions` adds your own notes to what agents read on connecting, e.g. which of your shops take what.
78
- app.all("/mcp", (c) => mcp(c.req.raw)); // Streamable HTTP, stateless, any Request/Response runtime
79
- ```
80
-
81
- Node 22.13 or later. Licence: FSL-1.1-ALv2 (free to use and self-host; becomes Apache-2.0 two years after
82
- each release).
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
- /** Where tools hand their card data: the card reads it, the model doesn't need it. */
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 as SVG, ink on transparent, so the tile behind it sets the colour. */
22
- qrSvg?: string;
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. 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.
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
- /** Where tools hand their card data: the card reads it, the model doesn't need it. */
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,.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}}
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: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}
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:12px;animation:ring 1.8s ease-out infinite;pointer-events:none}
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: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}
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:20px;font-weight:600;overflow-wrap:anywhere}
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;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)}
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
- .row{display:flex;gap:12px;align-items:center;margin-top:12px;flex-wrap:wrap}
54
- button{font:inherit;font-size:14px;border-radius:10px;padding:10px 14px;cursor:pointer;border:1px solid var(--line);background:var(--surface);color:var(--text)}
55
- button.pay{background:var(--lime);color:var(--ink);border-color:var(--lime);font-weight:600}
56
- button.link{background:none;border:none;padding:0;color:var(--muted);text-decoration:underline;font-size:13px}
57
- .foot{font-size:11px;color:var(--muted);margin-top:12px;display:flex;justify-content:space-between;align-items:center;gap:8px}
58
- .foot button.link{font-size:12px}
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) for (var k in vars) if (k.indexOf("--") === 0 && typeof vars[k] === "string") document.documentElement.style.setProperty(k, vars[k]);
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
- // A host that drops _meta: the JSON after the tool's summary line still has the basics.
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 safeSvg(svg) { return typeof svg === "string" && /^<svg[\s>]/.test(svg) && !/<script|<foreignObject|\son[a-z]+=/i.test(svg) ? svg : ""; }
211
+ function qrSvg(qr) {
212
+ if (!qr || typeof qr.bits !== "string" || !(qr.size > 0 && qr.size < 200)) return "";
213
+ try {
214
+ return '<svg viewBox="0 0 ' + qr.size + " " + qr.size + '" shape-rendering="crispEdges" role="img" aria-label="Pickup QR code"><path fill="#0a0b0d" d="' + qrPathFromBits(qr) + '"/></svg>';
215
+ } catch (e) { return ""; }
216
+ }
176
217
  function message(s) {
177
218
  var live = canCall && s.claim;
178
219
  switch (s.status) {
179
- case "awaiting_payment":
180
- return s.checkoutUrl
181
- ? "Pay on the checkout page. " + (live ? "This card turns into your pickup pass once it's paid." : "Then ask for the pickup pass.")
182
- : "Waiting for the payment. " + (live ? "This card turns into your pickup pass once it's paid." : "");
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 s.collectedAt ? "Collected at " + time(s.collectedAt) + ". Enjoy." : "Collected. Enjoy.";
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 label = LABELS[status] || status;
241
+ var unpaid = status === "awaiting_payment";
194
242
  var live = status === "paid" || status === "ready";
195
- var h = '<div class="card">';
196
- h += '<div class="head"><div><p class="shop">' + esc(s.shopName || "Your pickup order") + "</p>";
197
- if (s.items) h += '<p class="items">' + esc(s.items) + (s.total ? " · " + esc(s.total) : "") + "</p>";
198
- h += '</div><span class="badge' + (status === "ready" ? " ready" : "") + '">' + esc(label) + "</span></div>";
199
- var svg = safeSvg(s.qrSvg);
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>&#10003;</b>Collected</div></div></div>';
263
+ h += '<div class="wrap"><div class="tile plain"><div class="done"><b>&#10003;</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
- if (live || status === "collected") {
213
- var at = { paid: 0, ready: 1, collected: 2 }[status];
214
- h += '<div class="steps">' + ["Paid", "Ready", "Collected"].map(function (name, i) {
215
- return '<span class="step' + (i < at ? " past" : i === at ? " now" : "") + '"><span class="dot"></span>' + name + "</span>";
216
- }).join("") + "</div>";
217
- }
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
- var actions = "";
222
- if (status === "awaiting_payment" && s.checkoutUrl) actions += '<button class="pay" data-open="checkoutUrl">Pay' + (s.total ? " " + esc(s.total) : "") + " by card</button>";
223
- if (live && s.passUrl) actions += '<button class="link" data-open="passUrl">Open the pass</button>';
224
- if (actions) h += '<div class="row">' + actions + "</div>";
279
+ if (live && s.passUrl) h += '<div class="actions"><button class="link" data-open="passUrl">Open the pass</button></div>';
225
280
  h += '<div class="foot">' + (s.inviteUrl ? '<button class="link" data-open="inviteUrl">Invite a friend</button>' : "<span></span>") + "<span>agentPickup</span></div></div>";
226
281
  return h;
227
282
  }
@@ -266,5 +321,5 @@ button.link{background:none;border:none;padding:0;color:var(--muted);text-decora
266
321
  </html>`;
267
322
  /** The card's page. Its version goes in the handshake, so a host's logs say which card it ran. */
268
323
  export function passCardHtml(version) {
269
- return PAGE.replace("__VERSION__", version);
324
+ return PAGE.replace("__VERSION__", version).replace("__QR_PATH__", QR_PATH_JS);
270
325
  }
package/dist/server.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { type Fetch, type Payer } from "./wallet.js";
3
- export declare const VERSION = "0.4.3";
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.