@agentpickup/mcp 0.1.1 → 0.2.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 +65 -63
- package/dist/cli.js +4 -1
- package/dist/server.d.ts +7 -2
- package/dist/server.js +94 -17
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,63 +1,65 @@
|
|
|
1
|
-
# @agentpickup/mcp
|
|
2
|
-
|
|
3
|
-
An MCP server for [agentPickup](https://github.com/dav3trad3r/agentpickup) shops. It lets an AI agent
|
|
4
|
-
|
|
5
|
-
collect.
|
|
6
|
-
|
|
7
|
-
## Tools
|
|
8
|
-
|
|
9
|
-
| Tool | What it does |
|
|
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
|
-
|
|
1
|
+
# @agentpickup/mcp
|
|
2
|
+
|
|
3
|
+
An MCP server for [agentPickup](https://github.com/dav3trad3r/agentpickup) shops. It lets an AI agent find
|
|
4
|
+
pickup shops near its user, see what they sell, order, pay in USDC over x402, and hand its user the QR they
|
|
5
|
+
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. Needs the user's email, where the shop sends the receipt and pickup QR. A test shop marks it paid straight away, with no money moving |
|
|
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 |
|
|
15
|
+
| `order_status` | Waiting for payment, paid, ready, collected or expired |
|
|
16
|
+
| `get_pickup_pass` | Fetches the pass link, pickup code and QR again by signing in as the paying wallet (Sign-In-With-X) |
|
|
17
|
+
|
|
18
|
+
## Use it
|
|
19
|
+
|
|
20
|
+
Try it against the hosted demo shop first. It pays with the demo's own small wallet, within the demo's caps:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{ "mcpServers": { "agentpickup": { "type": "http", "url": "https://agentpickup.app/mcp" } } }
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or run it locally against any agentPickup shop, with your own wallet:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"agentpickup": {
|
|
32
|
+
"command": "npx",
|
|
33
|
+
"args": ["-y", "@agentpickup/mcp"],
|
|
34
|
+
"env": {
|
|
35
|
+
"AGENTPICKUP_SHOP_URL": "https://orders.yourshop.com",
|
|
36
|
+
"AGENTPICKUP_WALLET_KEY": "0x…",
|
|
37
|
+
"AGENTPICKUP_MAX_PER_ORDER": "$25"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
| Setting | Default | |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `AGENTPICKUP_SHOP_URL` | the demo shop | The shop to order from. Tool calls can also name another shop with `shop_url` |
|
|
47
|
+
| `AGENTPICKUP_WALLET_KEY` | none | An EVM private key holding USDC. Without it, `pay_order` only quotes |
|
|
48
|
+
| `AGENTPICKUP_MAX_PER_ORDER` | `$25` | The most the wallet pays for one order |
|
|
49
|
+
| `AGENTPICKUP_NETWORKS` | `eip155:84532` | Networks the wallet may pay on. Base Sepolia (test USDC) unless you add `eip155:8453` for Base mainnet |
|
|
50
|
+
| `AGENTPICKUP_DIRECTORY_URL` | `https://agentpickup.app/directory/v1/shops` | Where `find_shops` searches. `off` leaves the tool out |
|
|
51
|
+
|
|
52
|
+
Use a wallet that holds only what you're willing to let an agent spend. The tools tell the model to confirm
|
|
53
|
+
items and the total with the user before paying.
|
|
54
|
+
|
|
55
|
+
## Host it in your own server
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { createMcpHandler, walletPayer } from "@agentpickup/mcp";
|
|
59
|
+
|
|
60
|
+
const mcp = createMcpHandler({ shopUrl: "https://orders.yourshop.com" }); // no wallet: pay_order quotes
|
|
61
|
+
app.all("/mcp", (c) => mcp(c.req.raw)); // Streamable HTTP, stateless, any Request/Response runtime
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Node 22.13 or later. Licence: FSL-1.1-ALv2 (free to use and self-host; becomes Apache-2.0 two years after
|
|
65
|
+
each release).
|
package/dist/cli.js
CHANGED
|
@@ -17,6 +17,9 @@ const payer = key
|
|
|
17
17
|
networks: env.AGENTPICKUP_NETWORKS?.split(",").map((n) => n.trim()).filter(Boolean),
|
|
18
18
|
})
|
|
19
19
|
: undefined;
|
|
20
|
-
|
|
20
|
+
// The public agentPickup directory, for find_shops. "off" leaves the tool out.
|
|
21
|
+
const directory = env.AGENTPICKUP_DIRECTORY_URL || "https://agentpickup.app/directory/v1/shops";
|
|
22
|
+
const directoryUrl = directory === "off" ? undefined : directory;
|
|
23
|
+
const server = createPickupMcpServer({ shopUrl, allowOtherShops: true, payer, directoryUrl });
|
|
21
24
|
await server.connect(new StdioServerTransport());
|
|
22
25
|
console.error(`agentPickup MCP · shop ${shopUrl} · ${payer ? `wallet ${payer.address}` : "no wallet (pay_order returns the pay link)"}`);
|
package/dist/server.d.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
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.2.0";
|
|
4
4
|
export interface PickupMcpOptions {
|
|
5
5
|
/** The agentPickup shop this server orders from. */
|
|
6
6
|
shopUrl: string;
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Let tool calls name any agentPickup shop. Leave off for hosted servers, so callers can't point it anywhere:
|
|
9
|
+
* they can still name shops on the same host (venues at /v/<slug>), as find_shops returns them.
|
|
10
|
+
*/
|
|
8
11
|
allowOtherShops?: boolean;
|
|
12
|
+
/** An agentPickup directory (<host>/directory/v1/shops). Adds find_shops: shops near someone that sell what they want. */
|
|
13
|
+
directoryUrl?: string;
|
|
9
14
|
/** How requests reach the shop. Defaults to global fetch; a shop hosting its own server passes its app's fetch. */
|
|
10
15
|
fetch?: Fetch;
|
|
11
16
|
/** The wallet that pays. Without one, pay_order returns the price and pay link for another x402 client. */
|
package/dist/server.js
CHANGED
|
@@ -1,25 +1,32 @@
|
|
|
1
|
-
// The agentPickup MCP server: lets any MCP client (Claude, ChatGPT, Cursor, an agent framework) find
|
|
2
|
-
//
|
|
1
|
+
// The agentPickup MCP server: lets any MCP client (Claude, ChatGPT, Cursor, an agent framework) find pickup
|
|
2
|
+
// shops near someone, see what they sell, order, pay over x402, and hand its user the QR to collect with.
|
|
3
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
4
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { errorOf, usd } from "./wallet.js";
|
|
7
|
-
export const VERSION = "0.
|
|
7
|
+
export const VERSION = "0.2.0";
|
|
8
8
|
class ToolError extends Error {
|
|
9
9
|
}
|
|
10
|
-
const INSTRUCTIONS = `agentPickup lets you buy things online for the user to collect in person.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
const INSTRUCTIONS = `agentPickup lets you buy things online for the user to collect in person.
|
|
11
|
+
0. find_shops (when available): shops near the user that sell what they want. Pass a shop's shop_url to the other tools.
|
|
12
|
+
1. get_shop: see what the shop sells, its pickup location and how it takes payment.
|
|
13
|
+
2. create_pickup_order: needs the user's email, where the shop sends the pickup QR.
|
|
14
|
+
3. pay_order: pays from the configured wallet in USDC over x402. Confirm the items and total with the user first.
|
|
15
|
+
4. Give the user the pass link and pickup code. They show the QR at the counter to collect.
|
|
15
16
|
get_pickup_pass fetches the pass again later; order_status says whether it's ready or collected.`;
|
|
16
17
|
const money = (cents, currency) => new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(cents / 100);
|
|
17
18
|
const reply = (summary, data) => ({
|
|
18
19
|
content: [{ type: "text", text: `${summary}\n\n${JSON.stringify(data, null, 2)}` }],
|
|
19
20
|
});
|
|
20
21
|
const failure = (message) => ({ content: [{ type: "text", text: message }], isError: true });
|
|
22
|
+
/** A shop's base URL: shops can live under a path (https://host/v/cafe). A pasted API or pass link works too. */
|
|
23
|
+
const shopBase = (url) => {
|
|
24
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
25
|
+
return url.origin + (path.match(/^(.*)\/(?:api|p|c)\/.+$/)?.[1] ?? path.replace(/\/staff$/, ""));
|
|
26
|
+
};
|
|
21
27
|
export function createPickupMcpServer(opts) {
|
|
22
|
-
const
|
|
28
|
+
const homeUrl = new URL(opts.shopUrl);
|
|
29
|
+
const home = shopBase(homeUrl);
|
|
23
30
|
const doFetch = opts.fetch ?? ((req) => fetch(req));
|
|
24
31
|
function shopFor(shopUrl) {
|
|
25
32
|
if (!shopUrl)
|
|
@@ -31,13 +38,14 @@ export function createPickupMcpServer(opts) {
|
|
|
31
38
|
catch {
|
|
32
39
|
throw new ToolError(`"${shopUrl}" isn't a URL.`);
|
|
33
40
|
}
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
// Shops on this server's own host: the home shop and its venues.
|
|
42
|
+
if (url.origin === homeUrl.origin)
|
|
43
|
+
return shopBase(url);
|
|
36
44
|
if (!opts.allowOtherShops)
|
|
37
|
-
throw new ToolError(`This server orders from
|
|
45
|
+
throw new ToolError(`This server orders from shops on ${homeUrl.origin} only.`);
|
|
38
46
|
if (url.protocol !== "https:" && url.hostname !== "localhost")
|
|
39
47
|
throw new ToolError("Shops must be served over https.");
|
|
40
|
-
return url
|
|
48
|
+
return shopBase(url);
|
|
41
49
|
}
|
|
42
50
|
async function call(shop, path, init) {
|
|
43
51
|
const res = await doFetch(new Request(`${shop}${path}`, init));
|
|
@@ -56,10 +64,66 @@ export function createPickupMcpServer(opts) {
|
|
|
56
64
|
return failure(`Couldn't reach the shop: ${err instanceof Error ? err.message : String(err)}`);
|
|
57
65
|
}
|
|
58
66
|
};
|
|
59
|
-
const shopArg = opts.allowOtherShops
|
|
60
|
-
? { shop_url: z.string().optional().describe(`An agentPickup shop's URL. Defaults to ${home}.`) }
|
|
67
|
+
const shopArg = opts.allowOtherShops || opts.directoryUrl
|
|
68
|
+
? { shop_url: z.string().optional().describe(`An agentPickup shop's URL, as find_shops returns it. Defaults to ${home}.`) }
|
|
61
69
|
: {};
|
|
62
70
|
const server = new McpServer({ name: "agentpickup", title: "agentPickup", version: VERSION }, { instructions: INSTRUCTIONS });
|
|
71
|
+
if (opts.directoryUrl) {
|
|
72
|
+
const directoryUrl = opts.directoryUrl;
|
|
73
|
+
server.registerTool("find_shops", {
|
|
74
|
+
title: "Find pickup shops",
|
|
75
|
+
description: "Find agentPickup shops to order from: near a point (lat/lng), in a suburb or city (place), or selling something (query). " +
|
|
76
|
+
"Returns each shop's shop_url, pickup address, hours, whether it's open now, distance, and matching menu items.",
|
|
77
|
+
inputSchema: {
|
|
78
|
+
query: z.string().max(100).optional().describe('What the user wants, e.g. "flat white" or "croissant". Leave out to list everything.'),
|
|
79
|
+
place: z.string().max(100).optional().describe('A suburb, street or city to match against shop addresses, e.g. "Surry Hills".'),
|
|
80
|
+
lat: z.number().min(-90).max(90).optional().describe("Where the user is: latitude. Send with lng."),
|
|
81
|
+
lng: z.number().min(-180).max(180).optional().describe("Where the user is: longitude. Send with lat."),
|
|
82
|
+
radius_km: z.number().positive().max(100).optional().describe("With lat/lng: how far to look. Default 10."),
|
|
83
|
+
open_now: z.boolean().optional().describe("Only shops open now (shops without listed hours stay in)."),
|
|
84
|
+
limit: z.number().int().min(1).max(50).optional(),
|
|
85
|
+
},
|
|
86
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
87
|
+
}, guarded(async (args) => {
|
|
88
|
+
if ((args.lat === undefined) !== (args.lng === undefined))
|
|
89
|
+
throw new ToolError("Send lat and lng together.");
|
|
90
|
+
const url = new URL(directoryUrl);
|
|
91
|
+
const params = { q: args.query, place: args.place, lat: args.lat, lng: args.lng, radius_km: args.radius_km, limit: args.limit };
|
|
92
|
+
for (const [k, v] of Object.entries(params))
|
|
93
|
+
if (v !== undefined && v !== "")
|
|
94
|
+
url.searchParams.set(k, String(v));
|
|
95
|
+
if (args.open_now)
|
|
96
|
+
url.searchParams.set("open_now", "true");
|
|
97
|
+
const res = await doFetch(new Request(url));
|
|
98
|
+
if (!res.ok)
|
|
99
|
+
throw new ToolError(await errorOf(res, `The directory answered ${res.status}.`));
|
|
100
|
+
const { shops } = (await res.json());
|
|
101
|
+
if (!shops.length)
|
|
102
|
+
return reply("No agentPickup shops match. Try a wider radius, another place, or no query.", { shops: [] });
|
|
103
|
+
const lines = shops.map((s) => {
|
|
104
|
+
const facts = [
|
|
105
|
+
s.distanceKm !== undefined ? `${s.distanceKm} km` : null,
|
|
106
|
+
s.openNow === true ? "open now" : s.openNow === false ? "closed now" : null,
|
|
107
|
+
s.testOnly ? "test shop, no real money" : null,
|
|
108
|
+
].filter(Boolean);
|
|
109
|
+
const items = s.items.slice(0, 3).map((i) => `${i.name} ${money(i.priceCents, s.currency)}`).join(", ");
|
|
110
|
+
return `- ${s.name}${facts.length ? ` (${facts.join(", ")})` : ""}${s.location.address ? `, ${s.location.address}` : ""}${items ? `: ${items}` : ""}`;
|
|
111
|
+
});
|
|
112
|
+
return reply(`${shops.length} shop${shops.length === 1 ? "" : "s"}:\n${lines.join("\n")}\nPass a shop's shop_url to get_shop or create_pickup_order.`, {
|
|
113
|
+
shops: shops.map((s) => ({
|
|
114
|
+
shop_url: s.url,
|
|
115
|
+
name: s.name,
|
|
116
|
+
location: { name: s.location.name, address: s.location.address, hours: s.location.hours },
|
|
117
|
+
distanceKm: s.distanceKm,
|
|
118
|
+
openNow: s.openNow,
|
|
119
|
+
testOnly: s.testOnly,
|
|
120
|
+
payments: { x402: s.payments.x402?.network ?? null, test: s.payments.test },
|
|
121
|
+
items: s.items.map((i) => ({ id: i.id, name: i.name, price: money(i.priceCents, s.currency) })),
|
|
122
|
+
moreItems: Math.max(0, s.itemCount - s.items.length),
|
|
123
|
+
})),
|
|
124
|
+
});
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
63
127
|
server.registerTool("get_shop", {
|
|
64
128
|
title: "See what a shop sells",
|
|
65
129
|
description: "What the shop sells (item ids and prices), where to collect, and which payments it takes.",
|
|
@@ -78,7 +142,11 @@ export function createPickupMcpServer(opts) {
|
|
|
78
142
|
}));
|
|
79
143
|
const payments = data.payments;
|
|
80
144
|
return reply(`${merchant.name}${merchant.location?.name ? `, collect at ${merchant.location.name}` : ""}. ` +
|
|
81
|
-
(payments.x402
|
|
145
|
+
(payments.x402
|
|
146
|
+
? `Takes USDC over x402 on ${payments.x402.network}.`
|
|
147
|
+
: payments.test
|
|
148
|
+
? "A test shop: orders are marked paid without money moving."
|
|
149
|
+
: "Doesn't take agent payments."), { shop, merchant: merchant.name, location: merchant.location, currency, items, x402: payments.x402 ?? null, test: Boolean(payments.test) });
|
|
82
150
|
}));
|
|
83
151
|
server.registerTool("create_pickup_order", {
|
|
84
152
|
title: "Order for pickup",
|
|
@@ -95,13 +163,22 @@ export function createPickupMcpServer(opts) {
|
|
|
95
163
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
96
164
|
}, guarded(async (args) => {
|
|
97
165
|
const shop = shopFor(args.shop_url);
|
|
166
|
+
// Pay over x402 where the shop takes it; a test shop marks the order paid straight away.
|
|
167
|
+
const { payments } = (await call(shop, "/api/catalog"));
|
|
168
|
+
const rail = payments.x402 ? "x402" : payments.test ? "test" : null;
|
|
169
|
+
if (!rail)
|
|
170
|
+
throw new ToolError("This shop doesn't take agent payments.");
|
|
98
171
|
const data = await call(shop, "/api/orders", {
|
|
99
172
|
method: "POST",
|
|
100
173
|
headers: { "Content-Type": "application/json" },
|
|
101
|
-
body: JSON.stringify({ items: args.items, email: args.email, name: args.name, rail
|
|
174
|
+
body: JSON.stringify({ items: args.items, email: args.email, name: args.name, rail }),
|
|
102
175
|
});
|
|
103
176
|
const order = data.order;
|
|
104
177
|
const next = data.next;
|
|
178
|
+
if (rail === "test") {
|
|
179
|
+
return reply(`Test order ${order.id}: ${order.items.map((i) => `${i.qty}× ${i.name}`).join(", ")}, ${money(order.totalCents, order.currency)}, ` +
|
|
180
|
+
`marked paid with no money moving. The shop emails ${args.email} the receipt and pickup QR; the pass is at ${next.passUrl}.`, { shop, orderId: order.id, status: order.status, test: true, total: money(order.totalCents, order.currency), items: order.items, passUrl: next.passUrl });
|
|
181
|
+
}
|
|
105
182
|
return reply(`Order ${order.id}: ${order.items.map((i) => `${i.qty}× ${i.name}`).join(", ")}, ${money(order.totalCents, order.currency)}. ` +
|
|
106
183
|
`Confirm with the user, then call pay_order.`, { shop, orderId: order.id, total: money(order.totalCents, order.currency), items: order.items, payUrl: next.url, collectBy: order.collectBy });
|
|
107
184
|
}));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentpickup/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server for agentPickup: let an AI agent
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MCP server for agentPickup: let an AI agent find pickup shops near its user, order, pay over x402, and hand its user the QR to collect in person.",
|
|
5
5
|
"mcpName": "io.github.dav3trad3r/agentpickup",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|