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

142 lines
5.6 KiB
TypeScript

/**
* 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<ExchangeIntentsDoc>(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<ExchangeTradesDoc>(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<ExchangePoolsDoc>(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<ExchangeReputationDoc>(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) }] };
},
);
}