@guiie/buda-mcp 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/PUBLISH_CHECKLIST.md +62 -53
  3. package/dist/http.js +22 -0
  4. package/dist/index.js +19 -0
  5. package/dist/tools/arbitrage.d.ts +35 -0
  6. package/dist/tools/arbitrage.d.ts.map +1 -0
  7. package/dist/tools/arbitrage.js +142 -0
  8. package/dist/tools/balances.d.ts.map +1 -1
  9. package/dist/tools/balances.js +24 -4
  10. package/dist/tools/compare_markets.d.ts.map +1 -1
  11. package/dist/tools/compare_markets.js +11 -10
  12. package/dist/tools/market_summary.d.ts +43 -0
  13. package/dist/tools/market_summary.d.ts.map +1 -0
  14. package/dist/tools/market_summary.js +81 -0
  15. package/dist/tools/markets.d.ts.map +1 -1
  16. package/dist/tools/markets.js +4 -2
  17. package/dist/tools/orderbook.d.ts.map +1 -1
  18. package/dist/tools/orderbook.js +14 -4
  19. package/dist/tools/orders.d.ts.map +1 -1
  20. package/dist/tools/orders.js +41 -3
  21. package/dist/tools/price_history.js +14 -14
  22. package/dist/tools/spread.d.ts.map +1 -1
  23. package/dist/tools/spread.js +10 -8
  24. package/dist/tools/ticker.d.ts.map +1 -1
  25. package/dist/tools/ticker.js +24 -3
  26. package/dist/tools/trades.d.ts.map +1 -1
  27. package/dist/tools/trades.js +17 -3
  28. package/dist/tools/volume.d.ts.map +1 -1
  29. package/dist/tools/volume.js +21 -3
  30. package/dist/utils.d.ts +17 -0
  31. package/dist/utils.d.ts.map +1 -0
  32. package/dist/utils.js +21 -0
  33. package/marketplace/README.md +1 -1
  34. package/marketplace/claude-listing.md +26 -14
  35. package/marketplace/gemini-tools.json +41 -9
  36. package/marketplace/openapi.yaml +335 -119
  37. package/package.json +1 -1
  38. package/server.json +2 -2
  39. package/src/http.ts +27 -0
  40. package/src/index.ts +24 -0
  41. package/src/tools/arbitrage.ts +202 -0
  42. package/src/tools/balances.ts +27 -4
  43. package/src/tools/compare_markets.ts +11 -10
  44. package/src/tools/market_summary.ts +124 -0
  45. package/src/tools/markets.ts +4 -2
  46. package/src/tools/orderbook.ts +15 -4
  47. package/src/tools/orders.ts +45 -4
  48. package/src/tools/price_history.ts +17 -17
  49. package/src/tools/spread.ts +10 -8
  50. package/src/tools/ticker.ts +27 -3
  51. package/src/tools/trades.ts +18 -3
  52. package/src/tools/volume.ts +24 -3
  53. package/src/utils.ts +21 -0
  54. package/test/unit.ts +254 -0
package/src/http.ts CHANGED
@@ -13,10 +13,13 @@ import * as volume from "./tools/volume.js";
13
13
  import * as spread from "./tools/spread.js";
14
14
  import * as compareMarkets from "./tools/compare_markets.js";
15
15
  import * as priceHistory from "./tools/price_history.js";
16
+ import * as arbitrage from "./tools/arbitrage.js";
17
+ import * as marketSummary from "./tools/market_summary.js";
16
18
  import * as balances from "./tools/balances.js";
17
19
  import * as orders from "./tools/orders.js";
18
20
  import * as placeOrder from "./tools/place_order.js";
19
21
  import * as cancelOrder from "./tools/cancel_order.js";
22
+ import { handleMarketSummary } from "./tools/market_summary.js";
20
23
 
21
24
  const PORT = parseInt(process.env.PORT ?? "3000", 10);
22
25
 
@@ -39,6 +42,8 @@ const PUBLIC_TOOL_SCHEMAS = [
39
42
  spread.toolSchema,
40
43
  compareMarkets.toolSchema,
41
44
  priceHistory.toolSchema,
45
+ arbitrage.toolSchema,
46
+ marketSummary.toolSchema,
42
47
  ];
43
48
 
44
49
  const AUTH_TOOL_SCHEMAS = [
@@ -62,6 +67,8 @@ function createServer(): McpServer {
62
67
  spread.register(server, client, reqCache);
63
68
  compareMarkets.register(server, client, reqCache);
64
69
  priceHistory.register(server, client, reqCache);
70
+ arbitrage.register(server, client, reqCache);
71
+ marketSummary.register(server, client, reqCache);
65
72
 
66
73
  if (authEnabled) {
67
74
  balances.register(server, client);
@@ -114,6 +121,25 @@ function createServer(): McpServer {
114
121
  },
115
122
  );
116
123
 
124
+ server.resource(
125
+ "buda-summary",
126
+ new ResourceTemplate("buda://summary/{market}", { list: undefined }),
127
+ async (uri, params) => {
128
+ const marketId = (params.market as string).toUpperCase();
129
+ const result = await handleMarketSummary({ market_id: marketId }, client, reqCache);
130
+ const text = result.content[0].text;
131
+ return {
132
+ contents: [
133
+ {
134
+ uri: uri.href,
135
+ mimeType: "application/json",
136
+ text,
137
+ },
138
+ ],
139
+ };
140
+ },
141
+ );
142
+
117
143
  return server;
118
144
  }
119
145
 
@@ -140,6 +166,7 @@ app.get("/.well-known/mcp/server-card.json", (_req, res) => {
140
166
  resources: [
141
167
  { uri: "buda://markets", name: "All Buda.com markets", mimeType: "application/json" },
142
168
  { uri: "buda://ticker/{market}", name: "Ticker for a specific market", mimeType: "application/json" },
169
+ { uri: "buda://summary/{market}", name: "Full market summary with liquidity rating", mimeType: "application/json" },
143
170
  ],
144
171
  prompts: [],
145
172
  });
package/src/index.ts CHANGED
@@ -14,10 +14,13 @@ import * as volume from "./tools/volume.js";
14
14
  import * as spread from "./tools/spread.js";
15
15
  import * as compareMarkets from "./tools/compare_markets.js";
16
16
  import * as priceHistory from "./tools/price_history.js";
17
+ import * as arbitrage from "./tools/arbitrage.js";
18
+ import * as marketSummary from "./tools/market_summary.js";
17
19
  import * as balances from "./tools/balances.js";
18
20
  import * as orders from "./tools/orders.js";
19
21
  import * as placeOrder from "./tools/place_order.js";
20
22
  import * as cancelOrder from "./tools/cancel_order.js";
23
+ import { handleMarketSummary } from "./tools/market_summary.js";
21
24
 
22
25
  const client = new BudaClient(
23
26
  undefined,
@@ -39,6 +42,8 @@ volume.register(server, client, cache);
39
42
  spread.register(server, client, cache);
40
43
  compareMarkets.register(server, client, cache);
41
44
  priceHistory.register(server, client, cache);
45
+ arbitrage.register(server, client, cache);
46
+ marketSummary.register(server, client, cache);
42
47
 
43
48
  // Auth-gated tools — only registered when API credentials are present
44
49
  if (client.hasAuth()) {
@@ -92,5 +97,24 @@ server.resource(
92
97
  },
93
98
  );
94
99
 
100
+ server.resource(
101
+ "buda-summary",
102
+ new ResourceTemplate("buda://summary/{market}", { list: undefined }),
103
+ async (uri, params) => {
104
+ const marketId = (params.market as string).toUpperCase();
105
+ const result = await handleMarketSummary({ market_id: marketId }, client, cache);
106
+ const text = result.content[0].text;
107
+ return {
108
+ contents: [
109
+ {
110
+ uri: uri.href,
111
+ mimeType: "application/json",
112
+ text,
113
+ },
114
+ ],
115
+ };
116
+ },
117
+ );
118
+
95
119
  const transport = new StdioServerTransport();
96
120
  await server.connect(transport);
@@ -0,0 +1,202 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { BudaClient, BudaApiError } from "../client.js";
4
+ import { MemoryCache, CACHE_TTL } from "../cache.js";
5
+ import type { AllTickersResponse, Ticker } from "../types.js";
6
+
7
+ export const toolSchema = {
8
+ name: "get_arbitrage_opportunities",
9
+ description:
10
+ "Detects cross-country price discrepancies for a given asset across Buda's CLP, COP, and PEN markets, " +
11
+ "normalized to USDC. Fetches all relevant tickers, converts each local price to USDC using the " +
12
+ "current USDC-CLP / USDC-COP / USDC-PEN rates, then computes pairwise discrepancy percentages. " +
13
+ "Results above threshold_pct are returned sorted by opportunity size. Note: Buda taker fee is 0.8% " +
14
+ "per leg (~1.6% round-trip) — always deduct fees before acting on any discrepancy. " +
15
+ "Example: 'Is there an arbitrage opportunity for BTC between Chile and Peru right now?'",
16
+ inputSchema: {
17
+ type: "object" as const,
18
+ properties: {
19
+ base_currency: {
20
+ type: "string",
21
+ description: "Base asset to scan (e.g. 'BTC', 'ETH', 'XRP').",
22
+ },
23
+ threshold_pct: {
24
+ type: "number",
25
+ description:
26
+ "Minimum price discrepancy percentage to include in results (default: 0.5). " +
27
+ "Buda taker fee is 0.8% per leg, so a round-trip requires > 1.6% to be profitable.",
28
+ },
29
+ },
30
+ required: ["base_currency"],
31
+ },
32
+ };
33
+
34
+ interface ArbitrageOpportunity {
35
+ market_a: string;
36
+ market_b: string;
37
+ price_a_usdc: number;
38
+ price_b_usdc: number;
39
+ discrepancy_pct: number;
40
+ higher_market: string;
41
+ lower_market: string;
42
+ }
43
+
44
+ interface ArbitrageInput {
45
+ base_currency: string;
46
+ threshold_pct?: number;
47
+ }
48
+
49
+ export async function handleArbitrageOpportunities(
50
+ { base_currency, threshold_pct = 0.5 }: ArbitrageInput,
51
+ client: BudaClient,
52
+ cache: MemoryCache,
53
+ ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
54
+ try {
55
+ const base = base_currency.toUpperCase();
56
+ const data = await cache.getOrFetch<AllTickersResponse>(
57
+ "tickers:all",
58
+ CACHE_TTL.TICKER,
59
+ () => client.get<AllTickersResponse>("/tickers"),
60
+ );
61
+
62
+ const tickerMap = new Map<string, Ticker>();
63
+ for (const t of data.tickers) {
64
+ tickerMap.set(t.market_id, t);
65
+ }
66
+
67
+ // Find USDC conversion rates for each fiat currency
68
+ const usdcClpTicker = tickerMap.get("USDC-CLP");
69
+ const usdcCopTicker = tickerMap.get("USDC-COP");
70
+ const usdcPenTicker = tickerMap.get("USDC-PEN");
71
+
72
+ // Build list of markets for the requested base currency with USDC-normalized prices
73
+ interface MarketPrice {
74
+ market_id: string;
75
+ local_price: number;
76
+ usdc_rate: number;
77
+ price_usdc: number;
78
+ }
79
+
80
+ const marketPrices: MarketPrice[] = [];
81
+
82
+ const candidates: Array<{ suffix: string; usdcTicker: Ticker | undefined }> = [
83
+ { suffix: "CLP", usdcTicker: usdcClpTicker },
84
+ { suffix: "COP", usdcTicker: usdcCopTicker },
85
+ { suffix: "PEN", usdcTicker: usdcPenTicker },
86
+ ];
87
+
88
+ for (const { suffix, usdcTicker } of candidates) {
89
+ const marketId = `${base}-${suffix}`;
90
+ const baseTicker = tickerMap.get(marketId);
91
+
92
+ if (!baseTicker || !usdcTicker) continue;
93
+
94
+ const localPrice = parseFloat(baseTicker.last_price[0]);
95
+ const usdcRate = parseFloat(usdcTicker.last_price[0]);
96
+
97
+ if (isNaN(localPrice) || isNaN(usdcRate) || usdcRate === 0) continue;
98
+
99
+ marketPrices.push({
100
+ market_id: marketId,
101
+ local_price: localPrice,
102
+ usdc_rate: usdcRate,
103
+ price_usdc: localPrice / usdcRate,
104
+ });
105
+ }
106
+
107
+ if (marketPrices.length < 2) {
108
+ return {
109
+ content: [
110
+ {
111
+ type: "text",
112
+ text: JSON.stringify({
113
+ error: `Not enough markets found for base currency '${base}' to compute arbitrage. ` +
114
+ `Need at least 2 of: ${base}-CLP, ${base}-COP, ${base}-PEN with USDC rates available.`,
115
+ code: "INSUFFICIENT_MARKETS",
116
+ }),
117
+ },
118
+ ],
119
+ isError: true,
120
+ };
121
+ }
122
+
123
+ // Compute all pairwise discrepancies
124
+ const opportunities: ArbitrageOpportunity[] = [];
125
+
126
+ for (let i = 0; i < marketPrices.length; i++) {
127
+ for (let j = i + 1; j < marketPrices.length; j++) {
128
+ const a = marketPrices[i];
129
+ const b = marketPrices[j];
130
+ const minPrice = Math.min(a.price_usdc, b.price_usdc);
131
+ const discrepancyPct = (Math.abs(a.price_usdc - b.price_usdc) / minPrice) * 100;
132
+
133
+ if (discrepancyPct < threshold_pct) continue;
134
+
135
+ const higherMarket = a.price_usdc > b.price_usdc ? a.market_id : b.market_id;
136
+ const lowerMarket = a.price_usdc < b.price_usdc ? a.market_id : b.market_id;
137
+
138
+ opportunities.push({
139
+ market_a: a.market_id,
140
+ market_b: b.market_id,
141
+ price_a_usdc: parseFloat(a.price_usdc.toFixed(4)),
142
+ price_b_usdc: parseFloat(b.price_usdc.toFixed(4)),
143
+ discrepancy_pct: parseFloat(discrepancyPct.toFixed(4)),
144
+ higher_market: higherMarket,
145
+ lower_market: lowerMarket,
146
+ });
147
+ }
148
+ }
149
+
150
+ opportunities.sort((a, b) => b.discrepancy_pct - a.discrepancy_pct);
151
+
152
+ const result = {
153
+ base_currency: base,
154
+ threshold_pct,
155
+ markets_analyzed: marketPrices.map((m) => ({
156
+ market_id: m.market_id,
157
+ price_usdc: parseFloat(m.price_usdc.toFixed(4)),
158
+ local_price: m.local_price,
159
+ usdc_rate: m.usdc_rate,
160
+ })),
161
+ opportunities_found: opportunities.length,
162
+ opportunities,
163
+ fees_note:
164
+ "Buda taker fee is 0.8% per leg. A round-trip arbitrage (buy on one market, sell on another) " +
165
+ "costs approximately 1.6% in fees. Only discrepancies well above 1.6% are likely profitable.",
166
+ };
167
+
168
+ return {
169
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
170
+ };
171
+ } catch (err) {
172
+ const msg =
173
+ err instanceof BudaApiError
174
+ ? { error: err.message, code: err.status, path: err.path }
175
+ : { error: String(err), code: "UNKNOWN" };
176
+ return {
177
+ content: [{ type: "text", text: JSON.stringify(msg) }],
178
+ isError: true,
179
+ };
180
+ }
181
+ }
182
+
183
+ export function register(server: McpServer, client: BudaClient, cache: MemoryCache): void {
184
+ server.tool(
185
+ toolSchema.name,
186
+ toolSchema.description,
187
+ {
188
+ base_currency: z
189
+ .string()
190
+ .describe("Base asset to scan (e.g. 'BTC', 'ETH', 'XRP')."),
191
+ threshold_pct: z
192
+ .number()
193
+ .min(0)
194
+ .default(0.5)
195
+ .describe(
196
+ "Minimum price discrepancy percentage to include in results (default: 0.5). " +
197
+ "Buda taker fee is 0.8% per leg, so a round-trip requires > 1.6% to be profitable.",
198
+ ),
199
+ },
200
+ (args) => handleArbitrageOpportunities(args, client, cache),
201
+ );
202
+ }
@@ -1,13 +1,16 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { BudaClient, BudaApiError } from "../client.js";
3
+ import { flattenAmount } from "../utils.js";
3
4
  import type { BalancesResponse } from "../types.js";
4
5
 
5
6
  export const toolSchema = {
6
7
  name: "get_balances",
7
8
  description:
8
- "Get all currency balances for the authenticated Buda.com account. " +
9
- "Returns total, available, frozen, and pending withdrawal amounts per currency. " +
10
- "Requires BUDA_API_KEY and BUDA_API_SECRET environment variables.",
9
+ "Returns all currency balances for the authenticated Buda.com account as flat typed objects. " +
10
+ "Each currency entry includes total amount, available amount (not frozen), frozen amount, and " +
11
+ "pending withdrawal amount all as floats with separate _currency fields. " +
12
+ "Requires BUDA_API_KEY and BUDA_API_SECRET. " +
13
+ "Example: 'How much BTC do I have available to trade right now?'",
11
14
  inputSchema: {
12
15
  type: "object" as const,
13
16
  properties: {},
@@ -22,8 +25,28 @@ export function register(server: McpServer, client: BudaClient): void {
22
25
  async () => {
23
26
  try {
24
27
  const data = await client.get<BalancesResponse>("/balances");
28
+
29
+ const result = data.balances.map((b) => {
30
+ const amount = flattenAmount(b.amount);
31
+ const available = flattenAmount(b.available_amount);
32
+ const frozen = flattenAmount(b.frozen_amount);
33
+ const pending = flattenAmount(b.pending_withdraw_amount);
34
+
35
+ return {
36
+ id: b.id,
37
+ amount: amount.value,
38
+ amount_currency: amount.currency,
39
+ available_amount: available.value,
40
+ available_amount_currency: available.currency,
41
+ frozen_amount: frozen.value,
42
+ frozen_amount_currency: frozen.currency,
43
+ pending_withdraw_amount: pending.value,
44
+ pending_withdraw_amount_currency: pending.currency,
45
+ };
46
+ });
47
+
25
48
  return {
26
- content: [{ type: "text", text: JSON.stringify(data.balances, null, 2) }],
49
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
27
50
  };
28
51
  } catch (err) {
29
52
  const msg =
@@ -7,9 +7,10 @@ import type { AllTickersResponse } from "../types.js";
7
7
  export const toolSchema = {
8
8
  name: "compare_markets",
9
9
  description:
10
- "Compare ticker data for all trading pairs of a given base currency across Buda.com's " +
11
- "supported quote currencies (CLP, COP, PEN, BTC, USDC, ETH). " +
12
- "For example, passing 'BTC' returns side-by-side data for BTC-CLP, BTC-COP, BTC-PEN, etc.",
10
+ "Returns side-by-side ticker data for all trading pairs of a given base currency across Buda.com's " +
11
+ "supported quote currencies (CLP, COP, PEN, BTC, USDC, ETH). All prices are floats; " +
12
+ "price_change_24h and price_change_7d are floats in percent (e.g. 1.23 means +1.23%). " +
13
+ "Example: 'In which country is Bitcoin currently most expensive on Buda?'",
13
14
  inputSchema: {
14
15
  type: "object" as const,
15
16
  properties: {
@@ -67,16 +68,16 @@ export function register(server: McpServer, client: BudaClient, cache: MemoryCac
67
68
  base_currency: base,
68
69
  markets: matching.map((t) => ({
69
70
  market_id: t.market_id,
70
- last_price: t.last_price[0],
71
- currency: t.last_price[1],
72
- best_bid: t.max_bid ? t.max_bid[0] : null,
73
- best_ask: t.min_ask ? t.min_ask[0] : null,
74
- volume_24h: t.volume ? t.volume[0] : null,
71
+ last_price: parseFloat(t.last_price[0]),
72
+ last_price_currency: t.last_price[1],
73
+ best_bid: t.max_bid ? parseFloat(t.max_bid[0]) : null,
74
+ best_ask: t.min_ask ? parseFloat(t.min_ask[0]) : null,
75
+ volume_24h: t.volume ? parseFloat(t.volume[0]) : null,
75
76
  price_change_24h: t.price_variation_24h
76
- ? (parseFloat(t.price_variation_24h) * 100).toFixed(2) + "%"
77
+ ? parseFloat((parseFloat(t.price_variation_24h) * 100).toFixed(4))
77
78
  : null,
78
79
  price_change_7d: t.price_variation_7d
79
- ? (parseFloat(t.price_variation_7d) * 100).toFixed(2) + "%"
80
+ ? parseFloat((parseFloat(t.price_variation_7d) * 100).toFixed(4))
80
81
  : null,
81
82
  })),
82
83
  };
@@ -0,0 +1,124 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { BudaClient, BudaApiError } from "../client.js";
4
+ import { MemoryCache, CACHE_TTL } from "../cache.js";
5
+ import { validateMarketId } from "../validation.js";
6
+ import { flattenAmount, getLiquidityRating } from "../utils.js";
7
+ import type { TickerResponse, VolumeResponse } from "../types.js";
8
+
9
+ export const toolSchema = {
10
+ name: "get_market_summary",
11
+ description:
12
+ "One-call summary of everything relevant about a market: last price, best bid/ask, spread %, " +
13
+ "24h volume, 24h and 7d price change, and a liquidity_rating ('high' / 'medium' / 'low' based on " +
14
+ "spread thresholds: < 0.3% = high, 0.3–1% = medium, > 1% = low). All prices and volumes are floats. " +
15
+ "Best first tool to call when a user asks about any specific market. " +
16
+ "Example: 'Give me a complete overview of the BTC-CLP market right now.'",
17
+ inputSchema: {
18
+ type: "object" as const,
19
+ properties: {
20
+ market_id: {
21
+ type: "string",
22
+ description: "Market ID (e.g. 'BTC-CLP', 'ETH-COP', 'BTC-PEN').",
23
+ },
24
+ },
25
+ required: ["market_id"],
26
+ },
27
+ };
28
+
29
+ export interface MarketSummaryResult {
30
+ market_id: string;
31
+ last_price: number;
32
+ last_price_currency: string;
33
+ bid: number;
34
+ ask: number;
35
+ spread_pct: number;
36
+ volume_24h: number;
37
+ volume_24h_currency: string;
38
+ price_change_24h: number;
39
+ price_change_7d: number;
40
+ liquidity_rating: "high" | "medium" | "low";
41
+ }
42
+
43
+ interface MarketSummaryInput {
44
+ market_id: string;
45
+ }
46
+
47
+ export async function handleMarketSummary(
48
+ { market_id }: MarketSummaryInput,
49
+ client: BudaClient,
50
+ cache: MemoryCache,
51
+ ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
52
+ try {
53
+ const validationError = validateMarketId(market_id);
54
+ if (validationError) {
55
+ return {
56
+ content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_MARKET_ID" }) }],
57
+ isError: true,
58
+ };
59
+ }
60
+
61
+ const id = market_id.toLowerCase();
62
+
63
+ // Fetch ticker and volume in parallel
64
+ const [tickerData, volumeData] = await Promise.all([
65
+ cache.getOrFetch<TickerResponse>(
66
+ `ticker:${id}`,
67
+ CACHE_TTL.TICKER,
68
+ () => client.get<TickerResponse>(`/markets/${id}/ticker`),
69
+ ),
70
+ client.get<VolumeResponse>(`/markets/${id}/volume`),
71
+ ]);
72
+
73
+ const t = tickerData.ticker;
74
+ const v = volumeData.volume;
75
+
76
+ const lastPrice = flattenAmount(t.last_price);
77
+ const bid = parseFloat(t.max_bid[0]);
78
+ const ask = parseFloat(t.min_ask[0]);
79
+ const volume24h = flattenAmount(v.ask_volume_24h);
80
+
81
+ const spreadAbs = ask - bid;
82
+ const spreadPct = ask > 0 ? parseFloat(((spreadAbs / ask) * 100).toFixed(4)) : 0;
83
+
84
+ const result: MarketSummaryResult = {
85
+ market_id: t.market_id,
86
+ last_price: lastPrice.value,
87
+ last_price_currency: lastPrice.currency,
88
+ bid,
89
+ ask,
90
+ spread_pct: spreadPct,
91
+ volume_24h: volume24h.value,
92
+ volume_24h_currency: volume24h.currency,
93
+ price_change_24h: parseFloat((parseFloat(t.price_variation_24h) * 100).toFixed(4)),
94
+ price_change_7d: parseFloat((parseFloat(t.price_variation_7d) * 100).toFixed(4)),
95
+ liquidity_rating: getLiquidityRating(spreadPct),
96
+ };
97
+
98
+ return {
99
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
100
+ };
101
+ } catch (err) {
102
+ const msg =
103
+ err instanceof BudaApiError
104
+ ? { error: err.message, code: err.status, path: err.path }
105
+ : { error: String(err), code: "UNKNOWN" };
106
+ return {
107
+ content: [{ type: "text", text: JSON.stringify(msg) }],
108
+ isError: true,
109
+ };
110
+ }
111
+ }
112
+
113
+ export function register(server: McpServer, client: BudaClient, cache: MemoryCache): void {
114
+ server.tool(
115
+ toolSchema.name,
116
+ toolSchema.description,
117
+ {
118
+ market_id: z
119
+ .string()
120
+ .describe("Market ID (e.g. 'BTC-CLP', 'ETH-COP', 'BTC-PEN')."),
121
+ },
122
+ (args) => handleMarketSummary(args, client, cache),
123
+ );
124
+ }
@@ -8,8 +8,10 @@ import type { MarketsResponse, MarketResponse } from "../types.js";
8
8
  export const toolSchema = {
9
9
  name: "get_markets",
10
10
  description:
11
- "List all available trading pairs on Buda.com, or get details for a specific market. " +
12
- "Returns base/quote currencies, fees, and minimum order sizes.",
11
+ "Lists all available trading pairs on Buda.com, or returns details for a specific market " +
12
+ "(base/quote currencies, taker/maker fees as decimals, minimum order size in base currency, " +
13
+ "and fee discount tiers). Omit market_id to get all ~26 markets at once. " +
14
+ "Example: 'What is the taker fee and minimum order size for BTC-CLP?'",
13
15
  inputSchema: {
14
16
  type: "object" as const,
15
17
  properties: {
@@ -8,8 +8,10 @@ import type { OrderBookResponse } from "../types.js";
8
8
  export const toolSchema = {
9
9
  name: "get_orderbook",
10
10
  description:
11
- "Get the current order book (bids and asks) for a Buda.com market. Returns sorted arrays of " +
12
- "bids (buy orders) and asks (sell orders), each as [price, amount] pairs.",
11
+ "Returns the current order book for a Buda.com market as typed objects with float price and amount fields. " +
12
+ "Bids are sorted highest-price first; asks lowest-price first. " +
13
+ "Prices are in the quote currency; amounts are in the base currency. " +
14
+ "Example: 'What are the top 5 buy and sell orders for BTC-CLP right now?'",
13
15
  inputSchema: {
14
16
  type: "object" as const,
15
17
  properties: {
@@ -59,9 +61,18 @@ export function register(server: McpServer, client: BudaClient, cache: MemoryCac
59
61
  );
60
62
 
61
63
  const book = data.order_book;
64
+ const bids = limit ? book.bids.slice(0, limit) : book.bids;
65
+ const asks = limit ? book.asks.slice(0, limit) : book.asks;
66
+
62
67
  const result = {
63
- bids: limit ? book.bids.slice(0, limit) : book.bids,
64
- asks: limit ? book.asks.slice(0, limit) : book.asks,
68
+ bids: bids.map(([price, amount]) => ({
69
+ price: parseFloat(price),
70
+ amount: parseFloat(amount),
71
+ })),
72
+ asks: asks.map(([price, amount]) => ({
73
+ price: parseFloat(price),
74
+ amount: parseFloat(amount),
75
+ })),
65
76
  bid_count: book.bids.length,
66
77
  ask_count: book.asks.length,
67
78
  };
@@ -2,13 +2,17 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
3
  import { BudaClient, BudaApiError } from "../client.js";
4
4
  import { validateMarketId } from "../validation.js";
5
- import type { OrdersResponse } from "../types.js";
5
+ import { flattenAmount } from "../utils.js";
6
+ import type { OrdersResponse, Amount } from "../types.js";
6
7
 
7
8
  export const toolSchema = {
8
9
  name: "get_orders",
9
10
  description:
10
- "Get orders for a given Buda.com market. Filter by state (pending, active, traded, canceled). " +
11
- "Requires BUDA_API_KEY and BUDA_API_SECRET environment variables.",
11
+ "Returns orders for a given Buda.com market as flat typed objects. All monetary amounts are floats " +
12
+ "with separate _currency fields (e.g. amount + amount_currency). Filterable by state: pending, " +
13
+ "active, traded, canceled. Supports pagination via per and page. " +
14
+ "Requires BUDA_API_KEY and BUDA_API_SECRET. " +
15
+ "Example: 'Show my open limit orders on BTC-CLP.'",
12
16
  inputSchema: {
13
17
  type: "object" as const,
14
18
  properties: {
@@ -34,6 +38,10 @@ export const toolSchema = {
34
38
  },
35
39
  };
36
40
 
41
+ function flattenAmountField(amount: Amount): { value: number; currency: string } {
42
+ return flattenAmount(amount);
43
+ }
44
+
37
45
  export function register(server: McpServer, client: BudaClient): void {
38
46
  server.tool(
39
47
  toolSchema.name,
@@ -83,11 +91,44 @@ export function register(server: McpServer, client: BudaClient): void {
83
91
  Object.keys(params).length > 0 ? params : undefined,
84
92
  );
85
93
 
94
+ const orders = data.orders.map((o) => {
95
+ const amount = flattenAmountField(o.amount);
96
+ const originalAmount = flattenAmountField(o.original_amount);
97
+ const tradedAmount = flattenAmountField(o.traded_amount);
98
+ const totalExchanged = flattenAmountField(o.total_exchanged);
99
+ const paidFee = flattenAmountField(o.paid_fee);
100
+ const limitPrice = o.limit ? flattenAmountField(o.limit) : null;
101
+
102
+ return {
103
+ id: o.id,
104
+ type: o.type,
105
+ state: o.state,
106
+ created_at: o.created_at,
107
+ market_id: o.market_id,
108
+ fee_currency: o.fee_currency,
109
+ price_type: o.price_type,
110
+ order_type: o.order_type,
111
+ client_id: o.client_id,
112
+ limit_price: limitPrice ? limitPrice.value : null,
113
+ limit_price_currency: limitPrice ? limitPrice.currency : null,
114
+ amount: amount.value,
115
+ amount_currency: amount.currency,
116
+ original_amount: originalAmount.value,
117
+ original_amount_currency: originalAmount.currency,
118
+ traded_amount: tradedAmount.value,
119
+ traded_amount_currency: tradedAmount.currency,
120
+ total_exchanged: totalExchanged.value,
121
+ total_exchanged_currency: totalExchanged.currency,
122
+ paid_fee: paidFee.value,
123
+ paid_fee_currency: paidFee.currency,
124
+ };
125
+ });
126
+
86
127
  return {
87
128
  content: [
88
129
  {
89
130
  type: "text",
90
- text: JSON.stringify({ orders: data.orders, meta: data.meta }, null, 2),
131
+ text: JSON.stringify({ orders, meta: data.meta }, null, 2),
91
132
  },
92
133
  ],
93
134
  };