/** * MCP tools for rExchange (P2P trading). * * Tools: rexchange_list_intents, rexchange_list_trades, * rexchange_list_pools, rexchange_get_reputation */ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import type { SyncServer } from "../local-first/sync-server"; import { exchangeIntentsDocId, exchangeTradesDocId, exchangePoolsDocId, exchangeReputationDocId, } from "../../modules/rexchange/schemas"; import type { ExchangeIntentsDoc, ExchangeTradesDoc, ExchangePoolsDoc, ExchangeReputationDoc, } from "../../modules/rexchange/schemas"; import { resolveAccess, accessDeniedResponse } from "./_auth"; export function registerExchangeTools(server: McpServer, syncServer: SyncServer) { server.tool( "rexchange_list_intents", "List P2P trading intents (buy/sell offers)", { space: z.string().describe("Space slug"), token: z.string().optional().describe("JWT auth token"), side: z.string().optional().describe("Filter by side (buy or sell)"), status: z.string().optional().describe("Filter by status (active, matched, completed, cancelled, expired)"), limit: z.number().optional().describe("Max results (default 50)"), }, async ({ space, token, side, status, limit }) => { const access = await resolveAccess(token, space, false); if (!access.allowed) return accessDeniedResponse(access.reason!); const doc = syncServer.getDoc(exchangeIntentsDocId(space)); if (!doc) return { content: [{ type: "text", text: JSON.stringify({ error: "No exchange data found" }) }] }; let intents = Object.values(doc.intents || {}); if (side) intents = intents.filter(i => i.side === side); if (status) intents = intents.filter(i => i.status === status); intents.sort((a, b) => b.createdAt - a.createdAt); intents = intents.slice(0, limit || 50); const summary = intents.map(i => ({ id: i.id, creatorName: i.creatorName, side: i.side, tokenId: i.tokenId, fiatCurrency: i.fiatCurrency, tokenAmountMin: i.tokenAmountMin, tokenAmountMax: i.tokenAmountMax, rateType: i.rateType, rateFixed: i.rateFixed, paymentMethods: i.paymentMethods, status: i.status, isStandingOrder: i.isStandingOrder, createdAt: i.createdAt, })); return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] }; }, ); server.tool( "rexchange_list_trades", "List P2P trades with status and amounts", { space: z.string().describe("Space slug"), token: z.string().optional().describe("JWT auth token"), status: z.string().optional().describe("Filter by trade status"), limit: z.number().optional().describe("Max results (default 20)"), }, async ({ space, token, status, limit }) => { const access = await resolveAccess(token, space, false); if (!access.allowed) return accessDeniedResponse(access.reason!); const doc = syncServer.getDoc(exchangeTradesDocId(space)); if (!doc) return { content: [{ type: "text", text: JSON.stringify({ error: "No trades found" }) }] }; let trades = Object.values(doc.trades || {}); if (status) trades = trades.filter(t => t.status === status); trades.sort((a, b) => b.createdAt - a.createdAt); trades = trades.slice(0, limit || 20); const summary = trades.map(t => ({ id: t.id, buyerName: t.buyerName, sellerName: t.sellerName, tokenId: t.tokenId, tokenAmount: t.tokenAmount, fiatCurrency: t.fiatCurrency, fiatAmount: t.fiatAmount, agreedRate: t.agreedRate, paymentMethod: t.paymentMethod, status: t.status, chatMessageCount: t.chatMessages?.length ?? 0, createdAt: t.createdAt, completedAt: t.completedAt, })); return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] }; }, ); server.tool( "rexchange_list_pools", "List liquidity pool positions", { space: z.string().describe("Space slug"), token: z.string().optional().describe("JWT auth token"), }, async ({ space, token }) => { const access = await resolveAccess(token, space, false); if (!access.allowed) return accessDeniedResponse(access.reason!); const doc = syncServer.getDoc(exchangePoolsDocId(space)); if (!doc) return { content: [{ type: "text", text: JSON.stringify({ error: "No pools found" }) }] }; const positions = Object.values(doc.positions || {}).map(p => ({ id: p.id, creatorName: p.creatorName, tokenId: p.tokenId, fiatCurrency: p.fiatCurrency, tokenRemaining: p.tokenRemaining, fiatRemaining: p.fiatRemaining, spreadBps: p.spreadBps, feesEarnedToken: p.feesEarnedToken, feesEarnedFiat: p.feesEarnedFiat, tradesMatched: p.tradesMatched, status: p.status, })); return { content: [{ type: "text", text: JSON.stringify(positions, null, 2) }] }; }, ); server.tool( "rexchange_get_reputation", "Get trader reputation scores", { space: z.string().describe("Space slug"), token: z.string().optional().describe("JWT auth token"), did: z.string().optional().describe("Filter by specific DID"), }, async ({ space, token, did }) => { const access = await resolveAccess(token, space, false); if (!access.allowed) return accessDeniedResponse(access.reason!); const doc = syncServer.getDoc(exchangeReputationDocId(space)); if (!doc) return { content: [{ type: "text", text: JSON.stringify({ error: "No reputation data found" }) }] }; let records = Object.values(doc.records || {}); if (did) records = records.filter(r => r.did === did); records.sort((a, b) => b.score - a.score); return { content: [{ type: "text", text: JSON.stringify(records, null, 2) }] }; }, ); }