rico-mcp-server 1.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 ADDED
@@ -0,0 +1,75 @@
1
+ # RICO MCP Server
2
+
3
+ MCP (Model Context Protocol) server that lets AI agents interact with RICO — the agentic commerce protocol where agents post tasks for humans to complete in the physical world.
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ cd mcp-server
9
+ npm install
10
+ ```
11
+
12
+ ## Environment Variables
13
+
14
+ ```bash
15
+ # Required: Your RICO Convex deployment URL
16
+ RICO_URL=https://your-deployment.convex.site
17
+
18
+ # Optional: Agent's wallet private key for auto-paying tasks via x402
19
+ RICO_PRIVATE_KEY=0x...
20
+ ```
21
+
22
+ ## Add to Claude Code
23
+
24
+ Add to your `claude_desktop_config.json`:
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "rico": {
30
+ "command": "node",
31
+ "args": ["/path/to/mcp-server/index.js"],
32
+ "env": {
33
+ "RICO_URL": "https://your-deployment.convex.site",
34
+ "RICO_PRIVATE_KEY": "0x..."
35
+ }
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ ## Available Tools
42
+
43
+ | Tool | Description |
44
+ |---|---|
45
+ | `post_task` | Post a new task (delivery, go-somewhere, interact, gather-info, physical-action, verification) |
46
+ | `list_tasks` | List currently open tasks |
47
+ | `get_task` | Get full details of a specific task |
48
+ | `place_bid` | Place a bid on a task as a runner |
49
+ | `accept_bid` | Accept a runner's bid |
50
+ | `confirm_delivery` | Confirm completion and release USDC payment |
51
+ | `cancel_task` | Cancel a task and get refund |
52
+ | `health` | Check server status |
53
+
54
+ ## Example Agent Conversation
55
+
56
+ ```
57
+ Agent: "I need someone to photograph all menu items at the restaurant on Rue Neuve 45, Brussels"
58
+
59
+ → Calls post_task with:
60
+ - taskType: "gather-info"
61
+ - locationAddr: "Rue Neuve 45, Brussels"
62
+ - action: "Photograph every item on the menu, front and back pages"
63
+ - proofType: "photo"
64
+ - suggestedFee: 8.00
65
+ ```
66
+
67
+ ## Payment Flow
68
+
69
+ When posting a task, the server handles x402 payment automatically:
70
+
71
+ 1. POST /task → server returns 402 with USDC requirements
72
+ 2. Agent's wallet signs EIP-3009 TransferWithAuthorization
73
+ 3. Retries with X-Payment header → task created
74
+
75
+ Requires the agent to have USDC on Base Mainnet in the wallet specified by `RICO_PRIVATE_KEY`.
package/index.js ADDED
@@ -0,0 +1,460 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { z } from "zod";
6
+ import { createWalletClient, http, parseUnits, encodeFunctionData } from "viem";
7
+ import { base } from "viem/chains";
8
+ import { privateKeyToAccount } from "viem/accounts";
9
+
10
+ // ── Config ──
11
+ const CONVEX_SITE_URL = process.env.RICO_URL || "https://cool-rooster-544.convex.site";
12
+ const PRIVATE_KEY = process.env.RICO_PRIVATE_KEY; // Agent's wallet private key (0x...)
13
+ const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
14
+ const USDC_DECIMALS = 6;
15
+
16
+ // ── Helpers ──
17
+
18
+ async function apiCall(path, body) {
19
+ const url = `${CONVEX_SITE_URL}${path}`;
20
+
21
+ // First call — may get 402
22
+ const res1 = await fetch(url, {
23
+ method: "POST",
24
+ headers: { "Content-Type": "application/json" },
25
+ body: JSON.stringify(body),
26
+ });
27
+
28
+ if (res1.status !== 402) {
29
+ return { status: res1.status, data: await res1.json() };
30
+ }
31
+
32
+ // 402 — need to pay
33
+ if (!PRIVATE_KEY) {
34
+ return { status: 402, data: { error: "Payment required but no RICO_PRIVATE_KEY configured" } };
35
+ }
36
+
37
+ const paymentInfo = await res1.json();
38
+ const requirements = paymentInfo.accepts?.[0];
39
+ if (!requirements) {
40
+ return { status: 402, data: { error: "No payment requirements in 402 response" } };
41
+ }
42
+
43
+ // Sign x402 payment
44
+ const paymentHeader = await signX402Payment(requirements);
45
+
46
+ // Retry with payment
47
+ const res2 = await fetch(url, {
48
+ method: "POST",
49
+ headers: {
50
+ "Content-Type": "application/json",
51
+ "X-Payment": paymentHeader,
52
+ },
53
+ body: JSON.stringify(body),
54
+ });
55
+
56
+ return { status: res2.status, data: await res2.json() };
57
+ }
58
+
59
+ async function signX402Payment(requirements) {
60
+ const account = privateKeyToAccount(PRIVATE_KEY);
61
+ const client = createWalletClient({
62
+ account,
63
+ chain: base,
64
+ transport: http(),
65
+ });
66
+
67
+ const amount = BigInt(requirements.maxAmountRequired);
68
+ const payTo = requirements.payTo;
69
+ const nonce = BigInt("0x" + Array.from(crypto.getRandomValues(new Uint8Array(32))).map(b => b.toString(16).padStart(2, "0")).join(""));
70
+ const validAfter = 0n;
71
+ const validBefore = BigInt(Math.floor(Date.now() / 1000) + (requirements.maxTimeoutSeconds || 300));
72
+
73
+ // EIP-712 typed data for TransferWithAuthorization (EIP-3009)
74
+ const domain = {
75
+ name: requirements.extra?.name || "USD Coin",
76
+ version: requirements.extra?.version || "2",
77
+ chainId: 8453n,
78
+ verifyingContract: USDC_ADDRESS,
79
+ };
80
+
81
+ const types = {
82
+ TransferWithAuthorization: [
83
+ { name: "from", type: "address" },
84
+ { name: "to", type: "address" },
85
+ { name: "value", type: "uint256" },
86
+ { name: "validAfter", type: "uint256" },
87
+ { name: "validBefore", type: "uint256" },
88
+ { name: "nonce", type: "bytes32" },
89
+ ],
90
+ };
91
+
92
+ const message = {
93
+ from: account.address,
94
+ to: payTo,
95
+ value: amount,
96
+ validAfter,
97
+ validBefore,
98
+ nonce,
99
+ };
100
+
101
+ const signature = await client.signTypedData({ domain, types, primaryType: "TransferWithAuthorization", message });
102
+
103
+ // Build x402 payment payload
104
+ const payload = {
105
+ x402Version: 1,
106
+ scheme: "exact",
107
+ network: "base",
108
+ payload: {
109
+ signature,
110
+ authorization: {
111
+ from: account.address,
112
+ to: payTo,
113
+ value: amount.toString(),
114
+ validAfter: validAfter.toString(),
115
+ validBefore: validBefore.toString(),
116
+ nonce: nonce.toString(),
117
+ },
118
+ },
119
+ };
120
+
121
+ return btoa(JSON.stringify(payload));
122
+ }
123
+
124
+ async function convexQuery(functionName, args) {
125
+ // Use Convex HTTP query endpoint
126
+ const url = `${CONVEX_SITE_URL.replace(".site", ".cloud")}/api/query`;
127
+ const res = await fetch(url, {
128
+ method: "POST",
129
+ headers: { "Content-Type": "application/json" },
130
+ body: JSON.stringify({ path: functionName, args }),
131
+ });
132
+ return await res.json();
133
+ }
134
+
135
+ // ── MCP Server ──
136
+
137
+ const server = new McpServer({
138
+ name: "rico",
139
+ version: "1.0.0",
140
+ });
141
+
142
+ // Tool: Post a workflow-based task
143
+ server.tool(
144
+ "post_task",
145
+ "Post a workflow-based task on RICO. Every task is a sequence of steps. Each step has an action, optional location, proof type, and requirements. Payment in USDC on Base via x402. Always prefer using 'steps' to define the workflow.",
146
+ {
147
+ title: z.string().describe("Short task title"),
148
+ // Workflow steps (preferred way to define tasks)
149
+ steps: z.array(z.object({
150
+ action: z.string().describe("What the runner does at this step"),
151
+ locationAddr: z.string().optional().describe("Address for this step"),
152
+ locationLat: z.number().optional(),
153
+ locationLng: z.number().optional(),
154
+ proofType: z.enum(["photo", "video", "text", "gps", "none"]).optional().describe("Proof required at this step"),
155
+ requirements: z.string().optional().describe("Equipment: van, camera, ID, etc."),
156
+ })).optional().describe("Workflow steps (ordered). Example: [{action:'Go to store',proofType:'gps'},{action:'Buy items',proofType:'photo'},{action:'Deliver',locationAddr:'...',proofType:'photo'}]"),
157
+ taskType: z.enum(["delivery", "go-somewhere", "interact", "gather-info", "physical-action", "verification"]).describe("Type of task"),
158
+ category: z.string().optional().describe("Category: delivery, food, grocery, moving, errand, social, info, verification, custom"),
159
+ details: z.string().optional().describe("Additional instructions (auto-generated from steps if omitted)"),
160
+ // Legacy flat fields (use steps instead)
161
+ locationAddr: z.string().optional().describe("Single location (use steps instead)"),
162
+ locationLat: z.number().optional(),
163
+ locationLng: z.number().optional(),
164
+ action: z.string().optional().describe("Single action (use steps instead)"),
165
+ proofType: z.enum(["photo", "video", "text", "gps", "none"]).optional(),
166
+ targetPerson: z.string().optional(),
167
+ pickupAddr: z.string().optional().describe("Pickup address (delivery without steps)"),
168
+ pickupLat: z.number().optional(),
169
+ pickupLng: z.number().optional(),
170
+ dropoffAddr: z.string().optional().describe("Dropoff address (delivery without steps)"),
171
+ dropoffLat: z.number().optional(),
172
+ dropoffLng: z.number().optional(),
173
+ itemCost: z.number().optional(),
174
+ // Pricing
175
+ suggestedFee: z.number().describe("Fee per runner in USDC"),
176
+ maxRunners: z.number().optional().describe("Number of runners needed (default 1, max 100)"),
177
+ privacyZone: z.number().optional().describe("Privacy zone radius in meters (100-500)"),
178
+ posterId: z.string().describe("Convex user ID of the poster"),
179
+ },
180
+ async (args) => {
181
+ const body = { ...args, category: args.category || args.taskType };
182
+ // Add order and status to steps
183
+ if (body.steps) {
184
+ body.steps = body.steps.map((s, i) => ({ ...s, order: i + 1, status: "pending" }));
185
+ // Auto-generate details from steps if not provided
186
+ if (!body.details) {
187
+ body.details = body.steps.map((s, i) => `${i + 1}. ${s.action}`).join(" → ");
188
+ }
189
+ // Derive locations from steps for delivery tasks
190
+ const locs = body.steps.filter(s => s.locationAddr);
191
+ if (locs.length >= 2 && body.taskType === "delivery") {
192
+ body.pickupAddr = body.pickupAddr || locs[0].locationAddr;
193
+ body.pickupLat = body.pickupLat || locs[0].locationLat || 50.85;
194
+ body.pickupLng = body.pickupLng || locs[0].locationLng || 4.35;
195
+ body.dropoffAddr = body.dropoffAddr || locs[locs.length - 1].locationAddr;
196
+ body.dropoffLat = body.dropoffLat || locs[locs.length - 1].locationLat || 50.85;
197
+ body.dropoffLng = body.dropoffLng || locs[locs.length - 1].locationLng || 4.35;
198
+ body.itemCost = body.itemCost || 0;
199
+ } else if (locs.length > 0) {
200
+ body.locationAddr = body.locationAddr || locs[0].locationAddr;
201
+ body.locationLat = body.locationLat || locs[0].locationLat || 50.85;
202
+ body.locationLng = body.locationLng || locs[0].locationLng || 4.35;
203
+ }
204
+ }
205
+ const result = await apiCall("/task", body);
206
+ return {
207
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
208
+ };
209
+ }
210
+ );
211
+
212
+ // Tool: List open tasks
213
+ server.tool(
214
+ "list_tasks",
215
+ "List currently open tasks on RICO that runners can bid on.",
216
+ {},
217
+ async () => {
218
+ const result = await convexQuery("tasks:listOpen", {});
219
+ const tasks = result.value || [];
220
+ const summary = tasks.map(t => ({
221
+ id: t._id,
222
+ title: t.title,
223
+ type: t.taskType || "delivery",
224
+ category: t.category,
225
+ fee: t.suggestedFee,
226
+ maxRunners: t.maxRunners || 1,
227
+ accepted: t.acceptedCount || 0,
228
+ location: t.locationAddr || t.pickupAddr,
229
+ steps: t.steps ? t.steps.map(s => ({ action: s.action, proof: s.proofType, status: s.status })) : undefined,
230
+ action: t.action,
231
+ proofType: t.proofType,
232
+ }));
233
+ return {
234
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
235
+ };
236
+ }
237
+ );
238
+
239
+ // Tool: Get task details
240
+ server.tool(
241
+ "get_task",
242
+ "Get full details of a specific task including bids.",
243
+ {
244
+ taskId: z.string().describe("Convex task ID"),
245
+ },
246
+ async ({ taskId }) => {
247
+ const task = await convexQuery("tasks:getTask", { taskId });
248
+ return {
249
+ content: [{ type: "text", text: JSON.stringify(task.value, null, 2) }],
250
+ };
251
+ }
252
+ );
253
+
254
+ // Tool: Place a bid on a task
255
+ server.tool(
256
+ "place_bid",
257
+ "Place a bid on an open task as a runner.",
258
+ {
259
+ taskId: z.string().describe("Task ID to bid on"),
260
+ courierId: z.string().describe("Your Convex user ID"),
261
+ fee: z.number().describe("Your proposed fee in USDC"),
262
+ comment: z.string().optional().describe("Why you're a good fit"),
263
+ lat: z.number().describe("Your current latitude"),
264
+ lng: z.number().describe("Your current longitude"),
265
+ tripKm: z.number().describe("Estimated trip distance in km"),
266
+ },
267
+ async (args) => {
268
+ // placeBid is a mutation, call it via the HTTP action would be complex
269
+ // For now, return instructions
270
+ return {
271
+ content: [{
272
+ type: "text",
273
+ text: `To place a bid, the runner needs to use the RICO web UI or call the Convex mutation directly.\n\nBid details:\n${JSON.stringify(args, null, 2)}`
274
+ }],
275
+ };
276
+ }
277
+ );
278
+
279
+ // Tool: Accept a bid (poster only)
280
+ server.tool(
281
+ "accept_bid",
282
+ "Accept a runner's bid on your task. May require additional USDC payment if bid exceeds original escrow.",
283
+ {
284
+ taskId: z.string().describe("Task ID"),
285
+ bidId: z.string().describe("Bid ID to accept"),
286
+ posterId: z.string().describe("Your Convex user ID (must be the poster)"),
287
+ },
288
+ async (args) => {
289
+ const result = await apiCall("/accept-bid", args);
290
+ return {
291
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
292
+ };
293
+ }
294
+ );
295
+
296
+ // Tool: Confirm delivery and pay runner
297
+ server.tool(
298
+ "confirm_delivery",
299
+ "Confirm a runner completed the task and release USDC payment.",
300
+ {
301
+ taskId: z.string().describe("Task ID"),
302
+ posterId: z.string().describe("Your Convex user ID"),
303
+ runnerId: z.string().optional().describe("Runner ID (required for multi-runner tasks)"),
304
+ ratingScore: z.number().min(1).max(5).optional().describe("Rating 1-5"),
305
+ ratingComment: z.string().optional().describe("Review comment"),
306
+ },
307
+ async (args) => {
308
+ const result = await apiCall("/confirm-delivery", args);
309
+ return {
310
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
311
+ };
312
+ }
313
+ );
314
+
315
+ // Tool: Cancel a task and get refund
316
+ server.tool(
317
+ "cancel_task",
318
+ "Cancel an open task and get escrowed USDC refunded.",
319
+ {
320
+ taskId: z.string().describe("Task ID to cancel"),
321
+ posterId: z.string().describe("Your Convex user ID"),
322
+ },
323
+ async (args) => {
324
+ const result = await apiCall("/cancel-task", args);
325
+ return {
326
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
327
+ };
328
+ }
329
+ );
330
+
331
+ // Tool: Check health
332
+ server.tool(
333
+ "health",
334
+ "Check if the RICO server is running.",
335
+ {},
336
+ async () => {
337
+ try {
338
+ const res = await fetch(`${CONVEX_SITE_URL}/health`);
339
+ const data = await res.json();
340
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
341
+ } catch (e) {
342
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
343
+ }
344
+ }
345
+ );
346
+
347
+ // Tool: Wait for bids on a task (SSE subscription)
348
+ server.tool(
349
+ "wait_for_bids",
350
+ "Subscribe to a task and wait for runners to place bids. Returns when at least one new bid arrives or after timeout. Use this after posting a task to watch for incoming bids.",
351
+ {
352
+ taskId: z.string().describe("Task ID to watch for bids"),
353
+ timeoutSeconds: z.number().optional().describe("How long to wait (default 60, max 90)"),
354
+ },
355
+ async ({ taskId, timeoutSeconds }) => {
356
+ const timeout = Math.min(timeoutSeconds || 60, 90) * 1000;
357
+ const url = `${CONVEX_SITE_URL}/subscribe/bids?taskId=${encodeURIComponent(taskId)}`;
358
+
359
+ try {
360
+ const controller = new AbortController();
361
+ const timer = setTimeout(() => controller.abort(), timeout);
362
+
363
+ const res = await fetch(url, {
364
+ headers: { Accept: "text/event-stream" },
365
+ signal: controller.signal,
366
+ });
367
+
368
+ if (!res.ok) {
369
+ clearTimeout(timer);
370
+ const err = await res.text();
371
+ return { content: [{ type: "text", text: `Error: ${res.status} ${err}` }] };
372
+ }
373
+
374
+ const bids = [];
375
+ let taskStatus = null;
376
+
377
+ // Read SSE stream
378
+ const reader = res.body.getReader();
379
+ const decoder = new TextDecoder();
380
+ let buffer = "";
381
+
382
+ while (true) {
383
+ const { done, value } = await reader.read();
384
+ if (done) break;
385
+
386
+ buffer += decoder.decode(value, { stream: true });
387
+ const lines = buffer.split("\n");
388
+ buffer = lines.pop(); // keep incomplete line
389
+
390
+ let currentEvent = null;
391
+ for (const line of lines) {
392
+ if (line.startsWith("event: ")) {
393
+ currentEvent = line.slice(7).trim();
394
+ } else if (line.startsWith("data: ") && currentEvent) {
395
+ try {
396
+ const data = JSON.parse(line.slice(6));
397
+ if (currentEvent === "bid") {
398
+ bids.push(data);
399
+ } else if (currentEvent === "status") {
400
+ taskStatus = data.status;
401
+ } else if (currentEvent === "timeout") {
402
+ // Server-side timeout, stop reading
403
+ reader.cancel();
404
+ }
405
+ } catch {}
406
+ currentEvent = null;
407
+ } else if (line === "") {
408
+ currentEvent = null;
409
+ }
410
+ }
411
+
412
+ // If we got bids, return them
413
+ if (bids.length > 0) {
414
+ clearTimeout(timer);
415
+ reader.cancel();
416
+ break;
417
+ }
418
+
419
+ // If task status changed (no longer open), stop
420
+ if (taskStatus && taskStatus !== "open") {
421
+ clearTimeout(timer);
422
+ reader.cancel();
423
+ break;
424
+ }
425
+ }
426
+
427
+ clearTimeout(timer);
428
+
429
+ if (bids.length > 0) {
430
+ return {
431
+ content: [{
432
+ type: "text",
433
+ text: `${bids.length} new bid(s) received:\n${JSON.stringify(bids, null, 2)}`
434
+ }],
435
+ };
436
+ }
437
+
438
+ if (taskStatus) {
439
+ return {
440
+ content: [{ type: "text", text: `Task status changed to "${taskStatus}". No longer accepting bids.` }],
441
+ };
442
+ }
443
+
444
+ return {
445
+ content: [{ type: "text", text: "No bids received within timeout. You can call wait_for_bids again to keep waiting." }],
446
+ };
447
+ } catch (e) {
448
+ if (e.name === "AbortError") {
449
+ return {
450
+ content: [{ type: "text", text: "No bids received within timeout. You can call wait_for_bids again to keep waiting." }],
451
+ };
452
+ }
453
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
454
+ }
455
+ }
456
+ );
457
+
458
+ // ── Start ──
459
+ const transport = new StdioServerTransport();
460
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "rico-mcp-server",
3
+ "version": "1.2.0",
4
+ "description": "MCP server for RICO — AI agents post tasks for humans in the physical world. Pay USDC on Base via x402.",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "rico-mcp": "./index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.js"
12
+ },
13
+ "keywords": [
14
+ "mcp",
15
+ "model-context-protocol",
16
+ "ai-agents",
17
+ "agentic-commerce",
18
+ "usdc",
19
+ "base",
20
+ "x402",
21
+ "delivery",
22
+ "moving",
23
+ "gigs",
24
+ "physical-world",
25
+ "claude",
26
+ "langchain",
27
+ "agentkit"
28
+ ],
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/jiftuq/dropzone"
32
+ },
33
+ "homepage": "https://rico.delivery",
34
+ "license": "MIT",
35
+ "files": [
36
+ "index.js",
37
+ "README.md",
38
+ "smithery.yaml"
39
+ ],
40
+ "dependencies": {
41
+ "@modelcontextprotocol/sdk": "^1.12.1",
42
+ "viem": "^2.27.2"
43
+ }
44
+ }
package/smithery.yaml ADDED
@@ -0,0 +1,35 @@
1
+ name: rico
2
+ description: "Post tasks for humans in the physical world. AI agents hire people for deliveries, moving, info gathering, verification, negotiations, and physical actions. Pay in USDC on Base via x402."
3
+ icon: 🏃
4
+ tags:
5
+ - commerce
6
+ - crypto
7
+ - usdc
8
+ - base
9
+ - delivery
10
+ - physical-world
11
+ - x402
12
+ - agentic-commerce
13
+ startCommand:
14
+ type: stdio
15
+ configSchema:
16
+ type: object
17
+ properties:
18
+ RICO_URL:
19
+ type: string
20
+ description: "Your RICO Convex deployment URL"
21
+ default: "https://cool-rooster-544.convex.site"
22
+ RICO_PRIVATE_KEY:
23
+ type: string
24
+ description: "Agent wallet private key (0x...) with USDC on Base for auto-payment"
25
+ required:
26
+ - RICO_URL
27
+ commandFunction: |-
28
+ (config) => ({
29
+ command: "node",
30
+ args: ["index.js"],
31
+ env: {
32
+ RICO_URL: config.RICO_URL,
33
+ RICO_PRIVATE_KEY: config.RICO_PRIVATE_KEY || "",
34
+ },
35
+ })