@agentpickup/mcp 0.4.4 → 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 +2 -0
- package/dist/card.js +1 -1
- package/dist/server.d.ts +7 -1
- package/dist/server.js +249 -162
- package/dist/share-card.d.ts +2 -2
- 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
|
@@ -17,6 +17,8 @@ export interface PassCard {
|
|
|
17
17
|
/** Lets the card call order_status itself while the order moves along. */
|
|
18
18
|
claim?: string;
|
|
19
19
|
status?: string;
|
|
20
|
+
/** A paid order staff have started. */
|
|
21
|
+
accepted?: boolean;
|
|
20
22
|
rail?: string;
|
|
21
23
|
/** "2× Flat white, 1× Croissant" */
|
|
22
24
|
items?: string;
|
package/dist/card.js
CHANGED
|
@@ -218,7 +218,7 @@ button{font:inherit;cursor:pointer}
|
|
|
218
218
|
var live = canCall && s.claim;
|
|
219
219
|
switch (s.status) {
|
|
220
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 "Paid. " + (live ? "This turns green when it's ready. " : "") + "The pass is in your email too.";
|
|
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.";
|
|
222
222
|
case "ready": return "Ready at the counter. Show the QR or say the code.";
|
|
223
223
|
case "collected": return "Enjoy. The receipt is in your email.";
|
|
224
224
|
case "expired": return "This order expired before anyone collected it.";
|
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.
|
package/dist/server.js
CHANGED
|
@@ -7,7 +7,7 @@ import { z } from "zod";
|
|
|
7
7
|
import { CARD_META_KEY, CARD_MIME_TYPE, passCardHtml } from "./card.js";
|
|
8
8
|
import { SHARE_META_KEY, shareCardHtml } from "./share-card.js";
|
|
9
9
|
import { errorOf, usd } from "./wallet.js";
|
|
10
|
-
export const VERSION = "0.
|
|
10
|
+
export const VERSION = "0.5.0";
|
|
11
11
|
// The pickup pass card (MCP Apps). The version is in the URI so hosts that cache cards pick up a new one.
|
|
12
12
|
const CARD_URI = `ui://agentpickup/pass-${VERSION}.html`;
|
|
13
13
|
const CARD_TOOL_META = { ui: { resourceUri: CARD_URI }, "ui/resourceUri": CARD_URI };
|
|
@@ -46,24 +46,39 @@ const qrBits = (content) => {
|
|
|
46
46
|
};
|
|
47
47
|
class ToolError extends Error {
|
|
48
48
|
}
|
|
49
|
-
const INSTRUCTIONS = `agentPickup lets you buy things online for the user to collect in person.
|
|
50
|
-
0. find_shops (when available): shops near the user that sell what they want. Pass a shop's shop_url to the other tools.
|
|
51
|
-
1. get_shop: see what the shop sells, its pickup location and how it takes payment.
|
|
52
|
-
2. create_pickup_order: needs the user's email; the shop emails the pickup pass there too. It returns a claim:
|
|
53
|
-
keep it with the order id.
|
|
54
|
-
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
|
|
55
|
-
own x402 wallet. Where the shop takes cards, payment: "card" on
|
|
56
|
-
create_pickup_order gives a checkout link for the user to pay on instead. Where it takes test payments (get_shop says
|
|
57
|
-
so), payment: "test" runs the whole flow with no money moving. Confirm the items and total with the user first.
|
|
58
|
-
4. The pass: where the chat shows cards, the order appears as a card that becomes the pickup pass (QR and code)
|
|
59
|
-
once it's paid and keeps itself up to date until it's collected. Otherwise give the user the pass link and pickup
|
|
60
|
-
code. They show the QR at the counter, or say the code, to collect.
|
|
61
|
-
get_pickup_pass (with the claim) fetches the pass again; order_status (with the claim) says whether it's ready.
|
|
62
|
-
share_agentpickup: when the user wants to share agentPickup itself, invite a friend or get friends using it, shows a
|
|
63
|
-
QR and link for them. That isn't a group order: start_group_order is only for ordering together at one shop.
|
|
64
|
-
Group orders (a coffee run): start_group_order gives a code to share; each person's agent calls join_group_order and
|
|
65
|
-
pays its own share with pay_order; at closing time the shop gets one combined order and the organiser gets its QR.
|
|
49
|
+
const INSTRUCTIONS = `agentPickup lets you buy things online for the user to collect in person.
|
|
50
|
+
0. find_shops (when available): shops near the user that sell what they want. Pass a shop's shop_url to the other tools.
|
|
51
|
+
1. get_shop: see what the shop sells, its pickup location and how it takes payment.
|
|
52
|
+
2. create_pickup_order: needs the user's email; the shop emails the pickup pass there too. It returns a claim:
|
|
53
|
+
keep it with the order id.
|
|
54
|
+
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
|
|
55
|
+
own x402 wallet. Where the shop takes cards, payment: "card" on
|
|
56
|
+
create_pickup_order gives a checkout link for the user to pay on instead. Where it takes test payments (get_shop says
|
|
57
|
+
so), payment: "test" runs the whole flow with no money moving. Confirm the items and total with the user first.
|
|
58
|
+
4. The pass: where the chat shows cards, the order appears as a card that becomes the pickup pass (QR and code)
|
|
59
|
+
once it's paid and keeps itself up to date until it's collected. Otherwise give the user the pass link and pickup
|
|
60
|
+
code. They show the QR at the counter, or say the code, to collect.
|
|
61
|
+
get_pickup_pass (with the claim) fetches the pass again; order_status (with the claim) says whether it's ready.
|
|
62
|
+
share_agentpickup: when the user wants to share agentPickup itself, invite a friend or get friends using it, shows a
|
|
63
|
+
QR and link for them. That isn't a group order: start_group_order is only for ordering together at one shop.
|
|
64
|
+
Group orders (a lunch or coffee run): start_group_order gives a code to share; each person's agent calls join_group_order and
|
|
65
|
+
pays its own share with pay_order; at closing time the shop gets one combined order and the organiser gets its QR.
|
|
66
66
|
group_order_status shows who's in; close_group_order closes it early.`;
|
|
67
|
+
const CARDS_INSTRUCTIONS = `agentPickup lets you order from local shops (cafés, bakeries, stores and more) for the user to collect in person.
|
|
68
|
+
0. find_shops (when available): shops near the user that sell what they want. Pass a shop's shop_url to the other tools.
|
|
69
|
+
1. get_shop: what the shop sells, where to collect, and how it takes payment.
|
|
70
|
+
2. create_pickup_order, once the user has agreed the items and total. It needs the user's email (the shop emails the
|
|
71
|
+
pickup pass there too) and returns a claim: keep it with the order id.
|
|
72
|
+
3. Payment: the user pays by card (or Apple Pay or Google Pay) on the shop's own checkout page, from the link
|
|
73
|
+
create_pickup_order returns or the Pay button on the order's card. Where the shop takes test payments,
|
|
74
|
+
payment: "test" runs the whole flow with no money moving.
|
|
75
|
+
4. The pass: where the chat shows cards, the order's card becomes the pickup pass (QR and code) once it's paid and
|
|
76
|
+
keeps itself up to date until it's collected. Otherwise give the user the pass link and pickup code. They show the
|
|
77
|
+
QR at the counter, or say the code, to collect.
|
|
78
|
+
get_pickup_pass (with the claim) fetches the pass again; order_status (with the claim) says whether it's ready.
|
|
79
|
+
share_agentpickup: when the user wants to share agentPickup itself or invite a friend, shows a QR and link for them.`;
|
|
80
|
+
/** x402 networks that move no real money (the server's directory uses the same list). */
|
|
81
|
+
const TEST_NETWORKS = new Set(["eip155:84532"]);
|
|
67
82
|
const money = (cents, currency) => new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(cents / 100);
|
|
68
83
|
/** A tool's answer, and the card's data where there's a card: in structuredContent, which hosts pass to the card. */
|
|
69
84
|
const reply = (summary, data, card) => ({
|
|
@@ -150,6 +165,7 @@ export function createPickupMcpServer(opts) {
|
|
|
150
165
|
...(typeof i.unitCents === "number" ? { price: money(i.unitCents * i.qty, order.currency) } : {}),
|
|
151
166
|
})),
|
|
152
167
|
status: order.status,
|
|
168
|
+
...(order.status === "paid" && order.acceptedAt ? { accepted: true } : {}),
|
|
153
169
|
rail: order.rail,
|
|
154
170
|
items: order.items.map((i) => `${i.qty}× ${i.name}`).join(", "),
|
|
155
171
|
total: money(order.totalCents, order.currency),
|
|
@@ -185,7 +201,9 @@ export function createPickupMcpServer(opts) {
|
|
|
185
201
|
version: VERSION,
|
|
186
202
|
websiteUrl: "https://agentpickup.app",
|
|
187
203
|
icons: [{ src: "https://agentpickup.app/logo.png", mimeType: "image/png", sizes: ["1024x1024"] }],
|
|
188
|
-
}, {
|
|
204
|
+
}, {
|
|
205
|
+
instructions: (opts.cardsOnly ? CARDS_INSTRUCTIONS : INSTRUCTIONS) + (opts.instructions ? `\n${opts.instructions}` : ""),
|
|
206
|
+
});
|
|
189
207
|
server.registerResource("pickup-pass", CARD_URI, {
|
|
190
208
|
title: "Pickup pass",
|
|
191
209
|
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.",
|
|
@@ -206,7 +224,7 @@ export function createPickupMcpServer(opts) {
|
|
|
206
224
|
description: "Find agentPickup shops to order from: near a point (lat/lng), in a suburb or city (place), or selling something (query). " +
|
|
207
225
|
"Returns each shop's shop_url, pickup address, hours, whether it's open now, distance, and matching menu items.",
|
|
208
226
|
inputSchema: {
|
|
209
|
-
query: z.string().max(100).optional().describe('What the user wants, e.g. "flat white" or "
|
|
227
|
+
query: z.string().max(100).optional().describe('What the user wants, e.g. "flat white", "sourdough" or "AA batteries". Leave out to list everything.'),
|
|
210
228
|
place: z.string().max(100).optional().describe('A suburb, street or city to match against shop addresses, e.g. "Surry Hills".'),
|
|
211
229
|
lat: z.number().min(-90).max(90).optional().describe("Where the user is: latitude. Send with lng."),
|
|
212
230
|
lng: z.number().min(-180).max(180).optional().describe("Where the user is: longitude. Send with lat."),
|
|
@@ -228,7 +246,12 @@ export function createPickupMcpServer(opts) {
|
|
|
228
246
|
const res = await doFetch(new Request(url));
|
|
229
247
|
if (!res.ok)
|
|
230
248
|
throw new ToolError(await errorOf(res, `The directory answered ${res.status}.`));
|
|
231
|
-
const
|
|
249
|
+
const all = (await res.json()).shops;
|
|
250
|
+
// Cards-only: the shops it can order from here, by card or with a test payment. A shop on a test network
|
|
251
|
+
// is there to try USDC, so it isn't one of them.
|
|
252
|
+
const shops = opts.cardsOnly
|
|
253
|
+
? all.filter((s) => s.payments.card || s.payments.cardTest || (s.payments.test && !(s.payments.x402 && TEST_NETWORKS.has(s.payments.x402.network))))
|
|
254
|
+
: all;
|
|
232
255
|
if (!shops.length)
|
|
233
256
|
return reply("No agentPickup shops match. Try a wider radius, another place, or no query.", { shops: [] });
|
|
234
257
|
const lines = shops.map((s) => {
|
|
@@ -236,6 +259,7 @@ export function createPickupMcpServer(opts) {
|
|
|
236
259
|
s.distanceKm !== undefined ? `${s.distanceKm} km` : null,
|
|
237
260
|
s.openNow === true ? "open now" : s.openNow === false ? "closed now" : null,
|
|
238
261
|
s.testOnly ? "test shop, no real money" : null,
|
|
262
|
+
opts.cardsOnly ? (s.payments.card ? "takes cards" : s.payments.cardTest ? "takes test cards" : null) : null,
|
|
239
263
|
s.source?.kind === "listed" ? `listed by ${s.source.domain}` : null,
|
|
240
264
|
s.order?.via === "ucp" ? "orders through its UCP checkout" : s.order?.via === "web" ? "orders on its own website" : null,
|
|
241
265
|
].filter(Boolean);
|
|
@@ -250,7 +274,9 @@ export function createPickupMcpServer(opts) {
|
|
|
250
274
|
distanceKm: s.distanceKm,
|
|
251
275
|
openNow: s.openNow,
|
|
252
276
|
testOnly: s.testOnly,
|
|
253
|
-
payments:
|
|
277
|
+
payments: opts.cardsOnly
|
|
278
|
+
? { card: s.payments.card || Boolean(s.payments.cardTest), ...(s.payments.cardTest ? { testCards: true } : {}), test: s.payments.test }
|
|
279
|
+
: { x402: s.payments.x402?.network ?? null, test: s.payments.test },
|
|
254
280
|
listedBy: s.source?.kind === "listed" ? s.source.domain : null,
|
|
255
281
|
// agentpickup: use get_shop and create_pickup_order. ucp or web: order there instead.
|
|
256
282
|
orderVia: s.order?.via ?? "agentpickup",
|
|
@@ -271,6 +297,11 @@ export function createPickupMcpServer(opts) {
|
|
|
271
297
|
const data = await call(shop, "/api/catalog");
|
|
272
298
|
const merchant = data.merchant;
|
|
273
299
|
rememberShop(shop, merchant.name, merchant.location);
|
|
300
|
+
const NETWORK_NAMES = { instagram: "Instagram", x: "X", facebook: "Facebook", tiktok: "TikTok" };
|
|
301
|
+
const socials = (merchant.socials ?? []).filter((s) => NETWORK_NAMES[s.network]);
|
|
302
|
+
const followNote = socials.length
|
|
303
|
+
? ` On ${socials.map((s) => `${NETWORK_NAMES[s.network]}${s.network === "facebook" ? "" : ` as @${s.handle}`}`).join(", ")}.`
|
|
304
|
+
: "";
|
|
274
305
|
const currency = String(data.currency ?? "usd");
|
|
275
306
|
const items = data.items.map((i) => ({
|
|
276
307
|
id: i.id,
|
|
@@ -281,36 +312,84 @@ export function createPickupMcpServer(opts) {
|
|
|
281
312
|
const payments = data.payments;
|
|
282
313
|
const cards = Boolean(payments.square || payments.card);
|
|
283
314
|
const ways = [
|
|
284
|
-
...(payments.x402 ? [`USDC over x402 on ${payments.x402.network}, paid by the agent`] : []),
|
|
315
|
+
...(payments.x402 && !opts.cardsOnly ? [`USDC over x402 on ${payments.x402.network}, paid by the agent`] : []),
|
|
285
316
|
...(cards ? ["cards, paid by the user on a checkout link"] : []),
|
|
286
317
|
...(payments.test ? ["test payments, with no money moving"] : []),
|
|
287
318
|
];
|
|
319
|
+
// Known hours: say whether it's open, so nobody orders from a closed shop.
|
|
320
|
+
const openNow = typeof data.openNow === "boolean" ? data.openNow : null;
|
|
321
|
+
const hoursNote = openNow === true ? "Open now. " : openNow === false ? `Closed now${data.opensNext ? `; opens ${data.opensNext}` : ""}: orders aren't taken until then. ` : "";
|
|
288
322
|
return reply(`${merchant.name}${merchant.location?.name ? `, collect at ${merchant.location.name}` : ""}. ` +
|
|
323
|
+
hoursNote +
|
|
289
324
|
(!ways.length
|
|
290
|
-
?
|
|
325
|
+
? opts.cardsOnly
|
|
326
|
+
? "Doesn't take card payments here."
|
|
327
|
+
: "Doesn't take agent payments."
|
|
291
328
|
: ways.length === 1 && payments.test
|
|
292
329
|
? "A test shop: orders are marked paid without money moving."
|
|
293
|
-
: `Takes ${ways.join("; ")}.`)
|
|
330
|
+
: `Takes ${ways.join("; ")}.`) +
|
|
331
|
+
followNote, {
|
|
332
|
+
shop,
|
|
333
|
+
merchant: merchant.name,
|
|
334
|
+
location: merchant.location,
|
|
335
|
+
...(socials.length ? { socials } : {}),
|
|
336
|
+
openNow,
|
|
337
|
+
...(openNow === false && data.opensNext ? { opensNext: data.opensNext } : {}),
|
|
338
|
+
currency,
|
|
339
|
+
items,
|
|
340
|
+
...(opts.cardsOnly ? {} : { x402: payments.x402 ?? null }),
|
|
341
|
+
cards,
|
|
342
|
+
test: Boolean(payments.test),
|
|
343
|
+
});
|
|
344
|
+
}));
|
|
345
|
+
server.registerTool("ask_about_shop", {
|
|
346
|
+
title: "Ask about a shop",
|
|
347
|
+
description: "Answer a question about a shop that its menu doesn't: dietary needs and allergies (gluten-free, vegan, nuts), reviews, " +
|
|
348
|
+
"opening hours, where it is, parking, wait times, prices. Use it whenever the user asks something like that about a shop. " +
|
|
349
|
+
"Send the question in a few words, without names or other personal details. It answers from what the shop has said, " +
|
|
350
|
+
"says plainly what the shop hasn't, and tells the shop which topics people ask about (never the question itself).",
|
|
351
|
+
inputSchema: { ...shopArg, question: z.string().min(2).max(300).describe('The question in a few words, e.g. "gluten-free options?" or "is there parking?"') },
|
|
352
|
+
annotations: { title: "Ask about a shop", readOnlyHint: true, openWorldHint: true },
|
|
353
|
+
}, guarded(async (args) => {
|
|
354
|
+
const shop = await shopFor(args.shop_url);
|
|
355
|
+
const data = await call(shop, "/api/question", {
|
|
356
|
+
method: "POST",
|
|
357
|
+
headers: { "Content-Type": "application/json" },
|
|
358
|
+
body: JSON.stringify({ question: args.question }),
|
|
359
|
+
});
|
|
360
|
+
const answers = data.answers ?? [];
|
|
361
|
+
return reply(`${String(data.shop ?? "The shop")}: ${answers.map((a) => a.text).join(" ") || "it hasn't said."}`, { shop, topics: data.topics, answers });
|
|
294
362
|
}));
|
|
295
363
|
server.registerTool("create_pickup_order", {
|
|
296
364
|
title: "Order for pickup",
|
|
297
|
-
description:
|
|
298
|
-
"
|
|
299
|
-
|
|
365
|
+
description: opts.cardsOnly
|
|
366
|
+
? "Create an order to collect in person, once the user has agreed the items and total. Returns the order id, total, a " +
|
|
367
|
+
"claim (keep it) and the shop's checkout link, where the user pays by card. The shop emails the pickup pass to `email` " +
|
|
368
|
+
"once it's paid, and where the chat shows cards the order shows as one that becomes the pass. At shops that take test " +
|
|
369
|
+
"payments, `payment: \"test\"` marks it paid with no money moving."
|
|
370
|
+
: "Create an order to collect in person. Returns the order id, total and a claim (keep it); pay it with pay_order. " +
|
|
371
|
+
"The shop emails the pickup pass to `email` once it's paid, and where the chat shows cards the order shows as one " +
|
|
372
|
+
"that becomes the pass. Where the shop takes cards, `payment: \"card\"` returns a checkout link for the user to pay on instead.",
|
|
300
373
|
inputSchema: {
|
|
301
374
|
items: z
|
|
302
375
|
.array(z.object({ id: z.string().describe("Item id from get_shop"), qty: z.number().int().min(1).max(20) }))
|
|
303
376
|
.min(1),
|
|
304
377
|
email: z.string().email().describe("The user's email. The pickup pass goes here too."),
|
|
305
378
|
name: z.string().max(100).optional().describe("Name for the order, shown to staff"),
|
|
306
|
-
payment:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
379
|
+
payment: opts.cardsOnly
|
|
380
|
+
? z
|
|
381
|
+
.enum(["card", "test"])
|
|
382
|
+
.optional()
|
|
383
|
+
.describe("card (the user pays on the shop's checkout link) or test (where the shop takes test payments: no money moves). Default: card where the shop takes it, else test.")
|
|
384
|
+
: z
|
|
385
|
+
.enum(["agent", "card", "test"])
|
|
386
|
+
.optional()
|
|
387
|
+
.describe("Who pays: agent (USDC over x402, or a test payment at a test shop), card (the user pays on a checkout link), " +
|
|
388
|
+
"or test (where the shop takes test payments: the whole flow with no money moving). Default: agent where the shop takes it, else card."),
|
|
311
389
|
...shopArg,
|
|
312
390
|
},
|
|
313
|
-
|
|
391
|
+
// It creates a real order (and, paid, a charge): Claude asks before it runs.
|
|
392
|
+
annotations: { title: "Order for pickup", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
314
393
|
_meta: CARD_TOOL_META,
|
|
315
394
|
}, guarded(async (args) => {
|
|
316
395
|
const shop = await shopFor(args.shop_url);
|
|
@@ -324,11 +403,13 @@ export function createPickupMcpServer(opts) {
|
|
|
324
403
|
const rail = args.payment === "test" ? (payments.test ? "test" : null)
|
|
325
404
|
: args.payment === "card" ? cardRail
|
|
326
405
|
: args.payment === "agent" ? agentRail
|
|
327
|
-
: (
|
|
406
|
+
: opts.cardsOnly ? (cardRail ?? (payments.test ? "test" : null))
|
|
407
|
+
: (agentRail ?? cardRail);
|
|
328
408
|
if (!rail) {
|
|
329
409
|
throw new ToolError(args.payment === "test" ? "This shop doesn't take test payments."
|
|
330
410
|
: args.payment === "card" ? "This shop doesn't take cards."
|
|
331
|
-
: "This shop doesn't take
|
|
411
|
+
: opts.cardsOnly ? "This shop doesn't take card payments here."
|
|
412
|
+
: "This shop doesn't take agent payments.");
|
|
332
413
|
}
|
|
333
414
|
const data = await call(shop, "/api/orders", {
|
|
334
415
|
method: "POST",
|
|
@@ -363,55 +444,56 @@ export function createPickupMcpServer(opts) {
|
|
|
363
444
|
}
|
|
364
445
|
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 }));
|
|
365
446
|
}));
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (status.order.status
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
`
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
(
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
(paid.
|
|
413
|
-
|
|
414
|
-
|
|
447
|
+
if (!opts.cardsOnly)
|
|
448
|
+
server.registerTool("pay_order", {
|
|
449
|
+
title: "Pay for an order",
|
|
450
|
+
description: opts.payer
|
|
451
|
+
? `Pay an order in USDC over x402 from wallet ${opts.payer.address}. This spends money: confirm the items and total with the user first. Returns the pickup pass link.`
|
|
452
|
+
: "No wallet is set up here, so this returns the price and pay link for another x402 client to pay.",
|
|
453
|
+
inputSchema: { order_id: z.string(), ...shopArg },
|
|
454
|
+
annotations: { title: "Pay for an order", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
455
|
+
}, guarded(async (args) => {
|
|
456
|
+
const shop = await shopFor(args.shop_url);
|
|
457
|
+
const status = (await call(shop, `/api/orders/${encodeURIComponent(args.order_id)}`));
|
|
458
|
+
if (status.order.rail === "square" || status.order.rail === "card") {
|
|
459
|
+
if (status.order.status !== "awaiting_payment")
|
|
460
|
+
return reply(`Order ${args.order_id} is already paid (${status.order.status}).`, { orderId: args.order_id, status: status.order.status });
|
|
461
|
+
return reply(status.checkoutUrl
|
|
462
|
+
? `This order is paid by card: give the user ${status.checkoutUrl} to pay on.`
|
|
463
|
+
: "This order is paid by card, on the checkout link create_pickup_order returned.", { orderId: args.order_id, payBy: "card", checkoutUrl: status.checkoutUrl ?? null });
|
|
464
|
+
}
|
|
465
|
+
const payUrl = `${shop}/api/orders/${encodeURIComponent(args.order_id)}/pay`;
|
|
466
|
+
if (!opts.payer) {
|
|
467
|
+
const res = await doFetch(new Request(payUrl, { method: "POST" }));
|
|
468
|
+
if (res.status !== 402)
|
|
469
|
+
throw new ToolError(await errorOf(res, "This order isn't waiting for an x402 payment."));
|
|
470
|
+
// x402 v2 carries the requirements in the PAYMENT-REQUIRED header; older servers put them in the body.
|
|
471
|
+
const header = res.headers.get("PAYMENT-REQUIRED");
|
|
472
|
+
const required = (header ? JSON.parse(Buffer.from(header, "base64").toString("utf8")) : await res.json());
|
|
473
|
+
const accept = required.accepts?.[0];
|
|
474
|
+
// Other ways to finish, for an agent with no wallet of its own (a chat app on the web, say).
|
|
475
|
+
const { payments } = (await call(shop, "/api/catalog").catch(() => ({ payments: {} })));
|
|
476
|
+
const others = [
|
|
477
|
+
...(payments.square || payments.card ? ['payment "card" gives the user a checkout link to pay on'] : []),
|
|
478
|
+
...(payments.test ? ['payment "test" runs the whole flow with no money moving'] : []),
|
|
479
|
+
];
|
|
480
|
+
return reply(`Waiting for payment: ${usd(accept?.amount)} USDC on ${accept?.network}. No wallet is set up in this server: pay the link ` +
|
|
481
|
+
`from the user's own x402 wallet (such as Coinbase's agent wallet), or set AGENTPICKUP_WALLET_KEY where this server runs.` +
|
|
482
|
+
(others.length ? ` Or create the order again with create_pickup_order: ${others.join("; ")}.` : ""), { orderId: args.order_id, payUrl, amount: usd(accept?.amount), network: accept?.network, asset: accept?.asset, payTo: accept?.payTo });
|
|
483
|
+
}
|
|
484
|
+
const paid = await opts.payer.pay({ id: args.order_id, payUrl }, doFetch);
|
|
485
|
+
if (!paid.ok) {
|
|
486
|
+
// A demo or test shop can still show the user the whole flow, with a test payment instead.
|
|
487
|
+
const { payments } = (await call(shop, "/api/catalog").catch(() => ({ payments: {} })));
|
|
488
|
+
throw new ToolError(paid.error +
|
|
489
|
+
(payments.test
|
|
490
|
+
? ' This shop also takes test payments, which move no money: call create_pickup_order again with payment "test" to run the whole flow.'
|
|
491
|
+
: ""));
|
|
492
|
+
}
|
|
493
|
+
return reply(`Paid ${paid.amount ?? ""} USDC. Pickup pass: ${paid.passUrl}` +
|
|
494
|
+
(paid.emailedTo ? ` (also emailed to ${paid.emailedTo})` : "") +
|
|
495
|
+
`. The user shows its QR at the counter.`, { orderId: args.order_id, ...paid, ok: undefined });
|
|
496
|
+
}));
|
|
415
497
|
server.registerTool("order_status", {
|
|
416
498
|
title: "Check an order",
|
|
417
499
|
description: "Whether an order is waiting for payment, paid, ready to collect, collected or expired. With the order's claim, " +
|
|
@@ -424,7 +506,7 @@ export function createPickupMcpServer(opts) {
|
|
|
424
506
|
const { order, checkoutUrl } = (await call(shop, `/api/orders/${encodeURIComponent(args.order_id)}`));
|
|
425
507
|
const words = {
|
|
426
508
|
awaiting_payment: "is waiting for payment",
|
|
427
|
-
paid: "is paid and
|
|
509
|
+
paid: order.acceptedAt ? "is paid and the shop is making it" : "is paid; the shop has it but hasn't started it yet",
|
|
428
510
|
ready: "is ready to collect",
|
|
429
511
|
collected: "has been collected",
|
|
430
512
|
expired: "expired before anyone collected it",
|
|
@@ -475,7 +557,7 @@ export function createPickupMcpServer(opts) {
|
|
|
475
557
|
server.registerTool("share_agentpickup", {
|
|
476
558
|
title: "Share agentPickup",
|
|
477
559
|
description: "A QR code and link the user can share so friends can add agentPickup to Claude, ChatGPT or Grok and order too. " +
|
|
478
|
-
"Use when the user asks how to share agentPickup, invite friends, or get others using it. Name a shop to share it with that
|
|
560
|
+
"Use when the user asks how to share agentPickup, invite friends, or get others using it. Name a shop to share it with that shop picked.",
|
|
479
561
|
inputSchema: { ...shopArg },
|
|
480
562
|
annotations: { title: "Share agentPickup", readOnlyHint: true, openWorldHint: false },
|
|
481
563
|
_meta: SHARE_TOOL_META,
|
|
@@ -494,7 +576,8 @@ export function createPickupMcpServer(opts) {
|
|
|
494
576
|
{
|
|
495
577
|
type: "text",
|
|
496
578
|
text: `Share this link: ${links.url}. A friend opens it (or scans its QR off the user's screen), adds agentPickup to Claude, ChatGPT or Grok, ` +
|
|
497
|
-
`then just asks for their usual${name ? ` at ${name}` : ""}. ${links.sharePageUrl} opens the phone's share sheet and has a sticker to print
|
|
579
|
+
`then just asks for their usual${name ? ` at ${name}` : ""}. ${links.sharePageUrl} opens the phone's share sheet and has a sticker to print. ` +
|
|
580
|
+
`News and new shops: @agentPickup on X, https://x.com/agentPickup.`,
|
|
498
581
|
},
|
|
499
582
|
],
|
|
500
583
|
structuredContent: { share: card },
|
|
@@ -509,83 +592,87 @@ export function createPickupMcpServer(opts) {
|
|
|
509
592
|
const paid = g.members.filter((m) => m.status !== "awaiting_payment" && m.status !== "cancelled");
|
|
510
593
|
return `${g.title} (code ${g.code}), ${closesIn(g)}: ${paid.length} of ${g.members.length} paid, ${money(g.paidCents, g.currency)} so far.`;
|
|
511
594
|
};
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
595
|
+
if (!opts.cardsOnly)
|
|
596
|
+
server.registerTool("start_group_order", {
|
|
597
|
+
title: "Start a group order",
|
|
598
|
+
description: "Start a group order (a lunch or coffee run) at a shop, when the user wants to order together with others now. Returns a code to " +
|
|
599
|
+
"share: each person's agent joins with it and pays for their own items. When it closes, the shop gets one combined order " +
|
|
600
|
+
"and the organiser is emailed its pickup QR. To share agentPickup itself with a friend, use share_agentpickup instead.",
|
|
601
|
+
inputSchema: {
|
|
602
|
+
organiser_name: z.string().min(1).max(60).describe("Who collects, shown to staff"),
|
|
603
|
+
organiser_email: z.string().email().describe("Where the group's pickup QR goes"),
|
|
604
|
+
title: z.string().max(60).optional().describe('e.g. "Friday coffee run"'),
|
|
605
|
+
closes_in_minutes: z.number().int().min(5).max(240).optional().describe("Default 15"),
|
|
606
|
+
...shopArg,
|
|
607
|
+
},
|
|
608
|
+
annotations: { title: "Start a group order", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
609
|
+
}, guarded(async (args) => {
|
|
610
|
+
const shop = await shopFor(args.shop_url);
|
|
611
|
+
const data = (await call(shop, "/api/groups", {
|
|
612
|
+
method: "POST",
|
|
613
|
+
headers: { "Content-Type": "application/json" },
|
|
614
|
+
body: JSON.stringify({ name: args.organiser_name, email: args.organiser_email, title: args.title, closesInMinutes: args.closes_in_minutes }),
|
|
615
|
+
}));
|
|
616
|
+
return reply(`Started ${data.group.title}: share code ${data.group.code}. Everyone's agent joins with join_group_order and the code; it ${closesIn(data.group)}. ` +
|
|
617
|
+
`Keep organiser_key to close it early.`, { shop, code: data.group.code, closesAt: data.group.closesAt, organiser_key: data.organiserKey });
|
|
531
618
|
}));
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
},
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
619
|
+
if (!opts.cardsOnly)
|
|
620
|
+
server.registerTool("join_group_order", {
|
|
621
|
+
title: "Join a group order",
|
|
622
|
+
description: "Add the user's items to a group order with its code. Returns the order id; pay it with pay_order before the group closes. " +
|
|
623
|
+
"The organiser collects everything, so the user gets a receipt, not a QR.",
|
|
624
|
+
inputSchema: {
|
|
625
|
+
code: z.string().min(4).max(12).describe("The group's code"),
|
|
626
|
+
items: z.array(z.object({ id: z.string().describe("Item id from get_shop"), qty: z.number().int().min(1).max(20) })).min(1),
|
|
627
|
+
email: z.string().email().describe("The user's email, for their receipt"),
|
|
628
|
+
name: z.string().min(1).max(60).describe("The user's name, shown to staff on their items"),
|
|
629
|
+
...shopArg,
|
|
630
|
+
},
|
|
631
|
+
annotations: { title: "Join a group order", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
632
|
+
}, guarded(async (args) => {
|
|
633
|
+
const shop = await shopFor(args.shop_url);
|
|
634
|
+
const { payments } = (await call(shop, "/api/catalog"));
|
|
635
|
+
const rail = payments.x402 ? "x402" : payments.test ? "test" : null;
|
|
636
|
+
if (!rail)
|
|
637
|
+
throw new ToolError("This shop doesn't take agent payments.");
|
|
638
|
+
const data = (await call(shop, `/api/groups/${encodeURIComponent(args.code)}/orders`, {
|
|
639
|
+
method: "POST",
|
|
640
|
+
headers: { "Content-Type": "application/json" },
|
|
641
|
+
body: JSON.stringify({ items: args.items, email: args.email, name: args.name, rail }),
|
|
642
|
+
}));
|
|
643
|
+
const what = `${data.order.items.map((i) => `${i.qty}× ${i.name}`).join(", ")}, ${money(data.order.totalCents, data.order.currency)}`;
|
|
644
|
+
return reply(rail === "test"
|
|
645
|
+
? `Added to ${data.group.title}: ${what}, paid with a test payment (no money moved). ${data.group.organiser} collects it.`
|
|
646
|
+
: `Added to ${data.group.title}: ${what}. Confirm with the user, then pay it with pay_order (order ${data.order.id}) before the group closes.`, { shop, orderId: data.order.id, group: groupSummary(data.group), payUrl: data.next.url ?? null, test: rail === "test" });
|
|
557
647
|
}));
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
:
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
648
|
+
if (!opts.cardsOnly)
|
|
649
|
+
server.registerTool("group_order_status", {
|
|
650
|
+
title: "Check a group order",
|
|
651
|
+
description: "Who's in a group order, what they ordered, who has paid, and whether it has closed and been collected.",
|
|
652
|
+
inputSchema: { code: z.string().min(4).max(12), ...shopArg },
|
|
653
|
+
annotations: { title: "Check a group order", readOnlyHint: true, openWorldHint: true },
|
|
654
|
+
}, guarded(async (args) => {
|
|
655
|
+
const shop = await shopFor(args.shop_url);
|
|
656
|
+
const { group } = (await call(shop, `/api/groups/${encodeURIComponent(args.code)}`));
|
|
657
|
+
return reply(groupSummary(group) + (group.combined ? ` The shop's order is ${group.combined.status}.` : ""), group);
|
|
658
|
+
}));
|
|
659
|
+
if (!opts.cardsOnly)
|
|
660
|
+
server.registerTool("close_group_order", {
|
|
661
|
+
title: "Close a group order now",
|
|
662
|
+
description: "Close a group order early, as its organiser. Unpaid shares are left out; the shop gets the rest as one order.",
|
|
663
|
+
inputSchema: { code: z.string().min(4).max(12), organiser_key: z.string().min(8), ...shopArg },
|
|
664
|
+
annotations: { title: "Close a group order now", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
665
|
+
}, guarded(async (args) => {
|
|
666
|
+
const shop = await shopFor(args.shop_url);
|
|
667
|
+
const { group } = (await call(shop, `/api/groups/${encodeURIComponent(args.code)}/close`, {
|
|
668
|
+
method: "POST",
|
|
669
|
+
headers: { "Content-Type": "application/json" },
|
|
670
|
+
body: JSON.stringify({ organiserKey: args.organiser_key }),
|
|
671
|
+
}));
|
|
672
|
+
return reply(group.status === "sent"
|
|
673
|
+
? `Closed: ${groupSummary(group)} The shop has the combined order, and ${group.organiser}'s pickup QR is on its way by email.`
|
|
674
|
+
: `Closed with nobody paid, so the group order was cancelled.`, group);
|
|
584
675
|
}));
|
|
585
|
-
return reply(group.status === "sent"
|
|
586
|
-
? `Closed: ${groupSummary(group)} The shop has the combined order, and ${group.organiser}'s pickup QR is on its way by email.`
|
|
587
|
-
: `Closed with nobody paid, so the group order was cancelled.`, group);
|
|
588
|
-
}));
|
|
589
676
|
return server;
|
|
590
677
|
}
|
|
591
678
|
/** A Streamable HTTP endpoint (stateless, JSON responses) for hosting the server, e.g. at /mcp. */
|
package/dist/share-card.d.ts
CHANGED
|
@@ -2,13 +2,13 @@ import { type QrBits } from "./card.js";
|
|
|
2
2
|
export declare const SHARE_META_KEY = "agentpickup/share";
|
|
3
3
|
export interface ShareCard {
|
|
4
4
|
v: 1;
|
|
5
|
-
/** The add page, maybe for one
|
|
5
|
+
/** The add page, maybe for one shop (?shop=). */
|
|
6
6
|
url: string;
|
|
7
7
|
/** The add page's share mode: the share sheet and the sticker. */
|
|
8
8
|
sharePageUrl: string;
|
|
9
9
|
/** The QR as a bit grid, for the white tile. */
|
|
10
10
|
qr: QrBits;
|
|
11
|
-
/** A
|
|
11
|
+
/** A shop to name, when the link is for one. */
|
|
12
12
|
shopName?: string;
|
|
13
13
|
}
|
|
14
14
|
export declare function shareCardHtml(version: string): string;
|
package/package.json
CHANGED
|
@@ -1,63 +1,66 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@agentpickup/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server for agentPickup:
|
|
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
|
-
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"@
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
|
|
63
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentpickup/mcp",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "MCP server for agentPickup: people order from real shops through their AI assistant, pay by card on the shop's own checkout (or USDC over x402), and collect in person with a QR.",
|
|
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
|
+
"local-shops",
|
|
40
|
+
"pos",
|
|
41
|
+
"ordering"
|
|
42
|
+
],
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/Tokenise-au/agentpickup-sdk.git",
|
|
46
|
+
"directory": "packages/mcp"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://agentpickup.app",
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/Tokenise-au/agentpickup-sdk/issues"
|
|
51
|
+
},
|
|
52
|
+
"author": "Dave Hallett",
|
|
53
|
+
"license": "FSL-1.1-ALv2",
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
59
|
+
"@x402/core": "2.29.0",
|
|
60
|
+
"@x402/evm": "2.29.0",
|
|
61
|
+
"@x402/extensions": "2.29.0",
|
|
62
|
+
"qrcode": "^1.5.4",
|
|
63
|
+
"viem": "^2.57.14",
|
|
64
|
+
"zod": "^4.6.7"
|
|
65
|
+
}
|
|
66
|
+
}
|