152 lines
5.0 KiB
TypeScript
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) }] };
|
|
},
|
|
);
|
|
}
|