/** * MCP tools for rCart (catalog, shopping carts, group buys, payments). * * Tools: rcart_list_catalog, rcart_list_carts, rcart_get_cart, rcart_list_group_buys */ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import type { SyncServer } from "../local-first/sync-server"; import { catalogDocId, shoppingCartIndexDocId } from "../../modules/rcart/schemas"; import type { CatalogDoc, ShoppingCartIndexDoc, ShoppingCartDoc, GroupBuyDoc } from "../../modules/rcart/schemas"; import { resolveAccess, accessDeniedResponse } from "./_auth"; const CART_PREFIX = ":cart:shopping:"; const GROUP_BUY_PREFIX = ":cart:group-buys:"; export function registerCartTools(server: McpServer, syncServer: SyncServer) { server.tool( "rcart_list_catalog", "List product catalog entries in a space", { space: z.string().describe("Space slug"), token: z.string().optional().describe("JWT auth token"), search: z.string().optional().describe("Search in title/tags"), limit: z.number().optional().describe("Max results (default 50)"), }, async ({ space, token, search, limit }) => { const access = await resolveAccess(token, space, false); if (!access.allowed) return accessDeniedResponse(access.reason!); const doc = syncServer.getDoc(catalogDocId(space)); if (!doc) { return { content: [{ type: "text", text: JSON.stringify({ error: "No catalog found" }) }] }; } let items = Object.values(doc.items || {}); if (search) { const q = search.toLowerCase(); items = items.filter(i => i.title.toLowerCase().includes(q) || i.tags.some(t => t.toLowerCase().includes(q)), ); } items = items.slice(0, limit || 50); const summary = items.map(i => ({ id: i.id, title: i.title, productType: i.productType, substrates: i.substrates, tags: i.tags, status: i.status, createdAt: i.createdAt, })); return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] }; }, ); server.tool( "rcart_list_carts", "List shopping carts in a space with status and funding", { space: z.string().describe("Space slug"), token: z.string().optional().describe("JWT auth token"), status: z.string().optional().describe("Filter by status (OPEN, FUNDING, FUNDED, ORDERED, CLOSED)"), }, async ({ space, token, status }) => { const access = await resolveAccess(token, space, false); if (!access.allowed) return accessDeniedResponse(access.reason!); const indexDoc = syncServer.getDoc(shoppingCartIndexDocId(space)); if (!indexDoc) { return { content: [{ type: "text", text: JSON.stringify({ error: "No carts found" }) }] }; } let carts = Object.entries(indexDoc.carts || {}).map(([id, c]) => ({ id, ...c })); if (status) carts = carts.filter(c => c.status === status); return { content: [{ type: "text", text: JSON.stringify(carts, null, 2) }] }; }, ); server.tool( "rcart_get_cart", "Get full shopping cart with items and contributions", { space: z.string().describe("Space slug"), token: z.string().optional().describe("JWT auth token"), cart_id: z.string().describe("Cart ID"), }, async ({ space, token, cart_id }) => { const access = await resolveAccess(token, space, false); if (!access.allowed) return accessDeniedResponse(access.reason!); const docId = `${space}${CART_PREFIX}${cart_id}`; const doc = syncServer.getDoc(docId); if (!doc?.cart) { return { content: [{ type: "text", text: JSON.stringify({ error: "Cart not found" }) }] }; } return { content: [{ type: "text", text: JSON.stringify({ cart: doc.cart, items: Object.values(doc.items || {}), contributions: Object.values(doc.contributions || {}), eventCount: doc.events?.length ?? 0, }, null, 2), }], }; }, ); server.tool( "rcart_list_group_buys", "List group buys with pledge tallies", { space: z.string().describe("Space slug"), token: z.string().optional().describe("JWT auth token"), status: z.string().optional().describe("Filter by status (OPEN, LOCKED, ORDERED, CANCELLED)"), }, async ({ space, token, status }) => { const access = await resolveAccess(token, space, false); if (!access.allowed) return accessDeniedResponse(access.reason!); const prefix = `${space}${GROUP_BUY_PREFIX}`; const docIds = syncServer.getDocIds().filter(id => id.startsWith(prefix)); let buys: any[] = []; for (const docId of docIds) { const doc = syncServer.getDoc(docId); if (!doc?.buy) continue; buys.push({ id: doc.buy.id, title: doc.buy.title, status: doc.buy.status, totalPledged: doc.buy.totalPledged, pledgeCount: Object.keys(doc.pledges || {}).length, tiers: doc.buy.tiers, closesAt: doc.buy.closesAt, createdAt: doc.buy.createdAt, }); } if (status) buys = buys.filter(b => b.status === status); return { content: [{ type: "text", text: JSON.stringify(buys, null, 2) }] }; }, ); }