rspace-online/server/mcp-tools/rcart.ts

152 lines
5.0 KiB
TypeScript

/**
* 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<CatalogDoc>(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<ShoppingCartIndexDoc>(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<ShoppingCartDoc>(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<GroupBuyDoc>(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) }] };
},
);
}