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