@guiie/buda-mcp 1.2.2 → 1.4.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 (79) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/PUBLISH_CHECKLIST.md +71 -63
  3. package/README.md +4 -4
  4. package/dist/http.js +39 -0
  5. package/dist/index.js +29 -0
  6. package/dist/tools/arbitrage.d.ts +35 -0
  7. package/dist/tools/arbitrage.d.ts.map +1 -0
  8. package/dist/tools/arbitrage.js +142 -0
  9. package/dist/tools/balances.d.ts.map +1 -1
  10. package/dist/tools/balances.js +24 -4
  11. package/dist/tools/calculate_position_size.d.ts +48 -0
  12. package/dist/tools/calculate_position_size.d.ts.map +1 -0
  13. package/dist/tools/calculate_position_size.js +111 -0
  14. package/dist/tools/compare_markets.d.ts.map +1 -1
  15. package/dist/tools/compare_markets.js +11 -10
  16. package/dist/tools/dead_mans_switch.d.ts +84 -0
  17. package/dist/tools/dead_mans_switch.d.ts.map +1 -0
  18. package/dist/tools/dead_mans_switch.js +236 -0
  19. package/dist/tools/market_sentiment.d.ts +30 -0
  20. package/dist/tools/market_sentiment.d.ts.map +1 -0
  21. package/dist/tools/market_sentiment.js +104 -0
  22. package/dist/tools/market_summary.d.ts +43 -0
  23. package/dist/tools/market_summary.d.ts.map +1 -0
  24. package/dist/tools/market_summary.js +81 -0
  25. package/dist/tools/markets.d.ts.map +1 -1
  26. package/dist/tools/markets.js +4 -2
  27. package/dist/tools/orderbook.d.ts.map +1 -1
  28. package/dist/tools/orderbook.js +14 -4
  29. package/dist/tools/orders.d.ts.map +1 -1
  30. package/dist/tools/orders.js +41 -3
  31. package/dist/tools/price_history.d.ts.map +1 -1
  32. package/dist/tools/price_history.js +5 -43
  33. package/dist/tools/simulate_order.d.ts +45 -0
  34. package/dist/tools/simulate_order.d.ts.map +1 -0
  35. package/dist/tools/simulate_order.js +139 -0
  36. package/dist/tools/spread.d.ts.map +1 -1
  37. package/dist/tools/spread.js +10 -8
  38. package/dist/tools/technical_indicators.d.ts +39 -0
  39. package/dist/tools/technical_indicators.d.ts.map +1 -0
  40. package/dist/tools/technical_indicators.js +223 -0
  41. package/dist/tools/ticker.d.ts.map +1 -1
  42. package/dist/tools/ticker.js +24 -3
  43. package/dist/tools/trades.d.ts.map +1 -1
  44. package/dist/tools/trades.js +17 -3
  45. package/dist/tools/volume.d.ts.map +1 -1
  46. package/dist/tools/volume.js +21 -3
  47. package/dist/types.d.ts +9 -0
  48. package/dist/types.d.ts.map +1 -1
  49. package/dist/utils.d.ts +23 -0
  50. package/dist/utils.d.ts.map +1 -0
  51. package/dist/utils.js +68 -0
  52. package/marketplace/README.md +1 -1
  53. package/marketplace/claude-listing.md +60 -14
  54. package/marketplace/gemini-tools.json +183 -9
  55. package/marketplace/openapi.yaml +335 -119
  56. package/package.json +1 -1
  57. package/server.json +2 -2
  58. package/src/http.ts +44 -0
  59. package/src/index.ts +34 -0
  60. package/src/tools/arbitrage.ts +202 -0
  61. package/src/tools/balances.ts +27 -4
  62. package/src/tools/calculate_position_size.ts +141 -0
  63. package/src/tools/compare_markets.ts +11 -10
  64. package/src/tools/dead_mans_switch.ts +314 -0
  65. package/src/tools/market_sentiment.ts +141 -0
  66. package/src/tools/market_summary.ts +124 -0
  67. package/src/tools/markets.ts +4 -2
  68. package/src/tools/orderbook.ts +15 -4
  69. package/src/tools/orders.ts +45 -4
  70. package/src/tools/price_history.ts +5 -57
  71. package/src/tools/simulate_order.ts +182 -0
  72. package/src/tools/spread.ts +10 -8
  73. package/src/tools/technical_indicators.ts +282 -0
  74. package/src/tools/ticker.ts +27 -3
  75. package/src/tools/trades.ts +18 -3
  76. package/src/tools/volume.ts +24 -3
  77. package/src/types.ts +12 -0
  78. package/src/utils.ts +73 -0
  79. package/test/unit.ts +758 -0
@@ -0,0 +1,314 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { BudaClient, BudaApiError } from "../client.js";
4
+ import { validateMarketId } from "../validation.js";
5
+ import type { OrdersResponse, OrderResponse } from "../types.js";
6
+
7
+ // ---- Module-level timer state (persists across HTTP requests / tool invocations) ----
8
+
9
+ interface TimerEntry {
10
+ timeout: ReturnType<typeof setTimeout>;
11
+ expiresAt: number;
12
+ ttlSeconds: number;
13
+ }
14
+
15
+ const timers = new Map<string, TimerEntry>();
16
+
17
+ async function cancelAllOrdersForMarket(marketId: string, client: BudaClient): Promise<void> {
18
+ try {
19
+ const data = await client.get<OrdersResponse>(
20
+ `/markets/${marketId}/orders`,
21
+ { state: "pending", per: 300 },
22
+ );
23
+ const orders = data.orders ?? [];
24
+ await Promise.allSettled(
25
+ orders.map((order) =>
26
+ client.put<OrderResponse>(`/orders/${order.id}`, { state: "canceling" }),
27
+ ),
28
+ );
29
+ timers.delete(marketId);
30
+ } catch {
31
+ // Swallow errors — the timer has fired; we cannot surface them to the caller
32
+ timers.delete(marketId);
33
+ }
34
+ }
35
+
36
+ function armTimer(marketId: string, ttlSeconds: number, client: BudaClient): TimerEntry {
37
+ const existing = timers.get(marketId);
38
+ if (existing) clearTimeout(existing.timeout);
39
+
40
+ const expiresAt = Date.now() + ttlSeconds * 1000;
41
+ const timeout = setTimeout(() => {
42
+ void cancelAllOrdersForMarket(marketId, client);
43
+ }, ttlSeconds * 1000);
44
+
45
+ const entry: TimerEntry = { timeout, expiresAt, ttlSeconds };
46
+ timers.set(marketId, entry);
47
+ return entry;
48
+ }
49
+
50
+ // ---- Tool schemas ----
51
+
52
+ export const toolSchema = {
53
+ name: "schedule_cancel_all",
54
+ description:
55
+ "WARNING: timer state is lost on server restart. Not suitable as a production dead man's switch " +
56
+ "on hosted deployments (e.g. Railway). Use only on locally-run instances.\n\n" +
57
+ "Arms an in-memory dead man's switch: if not renewed within ttl_seconds, all open orders for the " +
58
+ "market are automatically cancelled. Requires confirmation_token='CONFIRM' to activate. " +
59
+ "Use renew_cancel_timer to reset the countdown, or disarm_cancel_timer to cancel without touching orders. " +
60
+ "Requires BUDA_API_KEY and BUDA_API_SECRET environment variables.",
61
+ inputSchema: {
62
+ type: "object" as const,
63
+ properties: {
64
+ market_id: {
65
+ type: "string",
66
+ description: "Market ID to protect (e.g. 'BTC-CLP').",
67
+ },
68
+ ttl_seconds: {
69
+ type: "number",
70
+ description: "Seconds before all orders are cancelled if not renewed (10–300).",
71
+ },
72
+ confirmation_token: {
73
+ type: "string",
74
+ description: "Must equal exactly 'CONFIRM' (case-sensitive) to arm the switch.",
75
+ },
76
+ },
77
+ required: ["market_id", "ttl_seconds", "confirmation_token"],
78
+ },
79
+ };
80
+
81
+ export const renewToolSchema = {
82
+ name: "renew_cancel_timer",
83
+ description:
84
+ "Resets the dead man's switch TTL for a market, preventing automatic order cancellation. " +
85
+ "No confirmation required. Requires an active timer set by schedule_cancel_all. " +
86
+ "Requires BUDA_API_KEY and BUDA_API_SECRET environment variables.",
87
+ inputSchema: {
88
+ type: "object" as const,
89
+ properties: {
90
+ market_id: {
91
+ type: "string",
92
+ description: "Market ID whose timer should be renewed (e.g. 'BTC-CLP').",
93
+ },
94
+ },
95
+ required: ["market_id"],
96
+ },
97
+ };
98
+
99
+ export const disarmToolSchema = {
100
+ name: "disarm_cancel_timer",
101
+ description:
102
+ "Disarms the dead man's switch for a market without cancelling any orders. " +
103
+ "No confirmation required. Safe to call even if no timer is active. " +
104
+ "Requires BUDA_API_KEY and BUDA_API_SECRET environment variables.",
105
+ inputSchema: {
106
+ type: "object" as const,
107
+ properties: {
108
+ market_id: {
109
+ type: "string",
110
+ description: "Market ID whose timer should be disarmed (e.g. 'BTC-CLP').",
111
+ },
112
+ },
113
+ required: ["market_id"],
114
+ },
115
+ };
116
+
117
+ // ---- Handlers (exported for unit tests) ----
118
+
119
+ type ScheduleArgs = {
120
+ market_id: string;
121
+ ttl_seconds: number;
122
+ confirmation_token: string;
123
+ };
124
+
125
+ export async function handleScheduleCancelAll(
126
+ args: ScheduleArgs,
127
+ client: BudaClient,
128
+ ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
129
+ const { market_id, ttl_seconds, confirmation_token } = args;
130
+
131
+ if (confirmation_token !== "CONFIRM") {
132
+ return {
133
+ content: [
134
+ {
135
+ type: "text",
136
+ text: JSON.stringify({
137
+ error:
138
+ "Dead man's switch not armed. confirmation_token must equal 'CONFIRM' to activate. " +
139
+ "Review the parameters and set confirmation_token='CONFIRM' to proceed.",
140
+ code: "CONFIRMATION_REQUIRED",
141
+ market_id,
142
+ ttl_seconds,
143
+ }),
144
+ },
145
+ ],
146
+ isError: true,
147
+ };
148
+ }
149
+
150
+ const validationError = validateMarketId(market_id);
151
+ if (validationError) {
152
+ return {
153
+ content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_MARKET_ID" }) }],
154
+ isError: true,
155
+ };
156
+ }
157
+
158
+ const id = market_id.toLowerCase();
159
+ const entry = armTimer(id, ttl_seconds, client);
160
+
161
+ return {
162
+ content: [
163
+ {
164
+ type: "text",
165
+ text: JSON.stringify({
166
+ active: true,
167
+ market_id: market_id.toUpperCase(),
168
+ expires_at: new Date(entry.expiresAt).toISOString(),
169
+ ttl_seconds,
170
+ warning: "in-memory only — timer is lost on server restart. Not suitable for hosted deployments.",
171
+ }),
172
+ },
173
+ ],
174
+ };
175
+ }
176
+
177
+ type MarketOnlyArgs = { market_id: string };
178
+
179
+ export function handleRenewCancelTimer(
180
+ { market_id }: MarketOnlyArgs,
181
+ client: BudaClient,
182
+ ): { content: Array<{ type: "text"; text: string }>; isError?: boolean } {
183
+ const validationError = validateMarketId(market_id);
184
+ if (validationError) {
185
+ return {
186
+ content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_MARKET_ID" }) }],
187
+ isError: true,
188
+ };
189
+ }
190
+
191
+ const id = market_id.toLowerCase();
192
+ const existing = timers.get(id);
193
+ if (!existing) {
194
+ return {
195
+ content: [
196
+ {
197
+ type: "text",
198
+ text: JSON.stringify({
199
+ error: `No active dead man's switch for market ${market_id.toUpperCase()}. Arm one first with schedule_cancel_all.`,
200
+ code: "NO_ACTIVE_TIMER",
201
+ market_id: market_id.toUpperCase(),
202
+ }),
203
+ },
204
+ ],
205
+ isError: true,
206
+ };
207
+ }
208
+
209
+ const entry = armTimer(id, existing.ttlSeconds, client);
210
+
211
+ return {
212
+ content: [
213
+ {
214
+ type: "text",
215
+ text: JSON.stringify({
216
+ active: true,
217
+ market_id: market_id.toUpperCase(),
218
+ expires_at: new Date(entry.expiresAt).toISOString(),
219
+ ttl_seconds: entry.ttlSeconds,
220
+ }),
221
+ },
222
+ ],
223
+ };
224
+ }
225
+
226
+ export function handleDisarmCancelTimer(
227
+ { market_id }: MarketOnlyArgs,
228
+ ): { content: Array<{ type: "text"; text: string }>; isError?: boolean } {
229
+ const validationError = validateMarketId(market_id);
230
+ if (validationError) {
231
+ return {
232
+ content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_MARKET_ID" }) }],
233
+ isError: true,
234
+ };
235
+ }
236
+
237
+ const id = market_id.toLowerCase();
238
+ const existing = timers.get(id);
239
+ if (!existing) {
240
+ return {
241
+ content: [
242
+ {
243
+ type: "text",
244
+ text: JSON.stringify({
245
+ disarmed: false,
246
+ market_id: market_id.toUpperCase(),
247
+ note: "No active timer for this market.",
248
+ }),
249
+ },
250
+ ],
251
+ };
252
+ }
253
+
254
+ clearTimeout(existing.timeout);
255
+ timers.delete(id);
256
+
257
+ return {
258
+ content: [
259
+ {
260
+ type: "text",
261
+ text: JSON.stringify({
262
+ disarmed: true,
263
+ market_id: market_id.toUpperCase(),
264
+ }),
265
+ },
266
+ ],
267
+ };
268
+ }
269
+
270
+ // ---- Registration ----
271
+
272
+ export function register(server: McpServer, client: BudaClient): void {
273
+ server.tool(
274
+ toolSchema.name,
275
+ toolSchema.description,
276
+ {
277
+ market_id: z
278
+ .string()
279
+ .describe("Market ID to protect (e.g. 'BTC-CLP')."),
280
+ ttl_seconds: z
281
+ .number()
282
+ .int()
283
+ .min(10)
284
+ .max(300)
285
+ .describe("Seconds before all orders are cancelled if not renewed (10–300)."),
286
+ confirmation_token: z
287
+ .string()
288
+ .describe("Must equal exactly 'CONFIRM' (case-sensitive) to arm the switch."),
289
+ },
290
+ (args) => handleScheduleCancelAll(args, client),
291
+ );
292
+
293
+ server.tool(
294
+ renewToolSchema.name,
295
+ renewToolSchema.description,
296
+ {
297
+ market_id: z
298
+ .string()
299
+ .describe("Market ID whose timer should be renewed (e.g. 'BTC-CLP')."),
300
+ },
301
+ (args) => handleRenewCancelTimer(args, client),
302
+ );
303
+
304
+ server.tool(
305
+ disarmToolSchema.name,
306
+ disarmToolSchema.description,
307
+ {
308
+ market_id: z
309
+ .string()
310
+ .describe("Market ID whose timer should be disarmed (e.g. 'BTC-CLP')."),
311
+ },
312
+ (args) => handleDisarmCancelTimer(args),
313
+ );
314
+ }
@@ -0,0 +1,141 @@
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 type { TickerResponse, VolumeResponse } from "../types.js";
7
+
8
+ export const toolSchema = {
9
+ name: "get_market_sentiment",
10
+ description:
11
+ "Computes a composite sentiment score (−100 to +100) for a Buda.com market based on " +
12
+ "24h price variation (40%), volume vs 7-day average (35%), and bid/ask spread vs baseline (25%). " +
13
+ "Returns a score, a label (bearish/neutral/bullish), and a full component breakdown. " +
14
+ "Example: 'Is the BTC-CLP market currently bullish or bearish?'",
15
+ inputSchema: {
16
+ type: "object" as const,
17
+ properties: {
18
+ market_id: {
19
+ type: "string",
20
+ description: "Market ID (e.g. 'BTC-CLP', 'ETH-BTC', 'BTC-USDT').",
21
+ },
22
+ },
23
+ required: ["market_id"],
24
+ },
25
+ };
26
+
27
+ function clamp(value: number, min: number, max: number): number {
28
+ return Math.min(max, Math.max(min, value));
29
+ }
30
+
31
+ function isStablecoinPair(marketId: string): boolean {
32
+ return /-(USDT|USDC|DAI|TUSD)$/i.test(marketId);
33
+ }
34
+
35
+ type MarketSentimentArgs = { market_id: string };
36
+
37
+ export async function handleMarketSentiment(
38
+ { market_id }: MarketSentimentArgs,
39
+ client: BudaClient,
40
+ cache: MemoryCache,
41
+ ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
42
+ const validationError = validateMarketId(market_id);
43
+ if (validationError) {
44
+ return {
45
+ content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_MARKET_ID" }) }],
46
+ isError: true,
47
+ };
48
+ }
49
+
50
+ try {
51
+ const id = market_id.toLowerCase();
52
+
53
+ const [tickerData, volumeData] = await Promise.all([
54
+ cache.getOrFetch<TickerResponse>(
55
+ `ticker:${id}`,
56
+ CACHE_TTL.TICKER,
57
+ () => client.get<TickerResponse>(`/markets/${id}/ticker`),
58
+ ),
59
+ client.get<VolumeResponse>(`/markets/${id}/volume`),
60
+ ]);
61
+
62
+ const ticker = tickerData.ticker;
63
+ const vol = volumeData.volume;
64
+
65
+ const bid = parseFloat(ticker.max_bid[0]);
66
+ const ask = parseFloat(ticker.min_ask[0]);
67
+ const priceVariation24h = parseFloat(ticker.price_variation_24h);
68
+
69
+ const ask24h = parseFloat(vol.ask_volume_24h[0]);
70
+ const bid24h = parseFloat(vol.bid_volume_24h[0]);
71
+ const ask7d = parseFloat(vol.ask_volume_7d[0]);
72
+ const bid7d = parseFloat(vol.bid_volume_7d[0]);
73
+
74
+ const spreadPct = ask > 0 ? ((ask - bid) / ask) * 100 : 0;
75
+ const spreadBaseline = isStablecoinPair(market_id) ? 0.3 : 1.0;
76
+
77
+ const volume24h = ask24h + bid24h;
78
+ const volume7d = ask7d + bid7d;
79
+ const volumeRatio = volume7d > 0 ? (volume24h * 7) / volume7d : 1;
80
+
81
+ // Price component: ±5% daily change → ±100 on this sub-score
82
+ const priceRaw = clamp(priceVariation24h * 2000, -100, 100);
83
+ const priceScore = parseFloat((priceRaw * 0.4).toFixed(4));
84
+
85
+ // Volume component: ratio vs 7d daily average
86
+ const volumeRaw = clamp((volumeRatio - 1) * 100, -100, 100);
87
+ const volumeScore = parseFloat((volumeRaw * 0.35).toFixed(4));
88
+
89
+ // Spread component: tighter spread is bullish
90
+ const spreadRaw = clamp((1 - spreadPct / spreadBaseline) * 100, -100, 100);
91
+ const spreadScore = parseFloat((spreadRaw * 0.25).toFixed(4));
92
+
93
+ const score = parseFloat((priceScore + volumeScore + spreadScore).toFixed(1));
94
+ const label: "bearish" | "neutral" | "bullish" =
95
+ score < -20 ? "bearish" : score > 20 ? "bullish" : "neutral";
96
+
97
+ const result = {
98
+ market_id: ticker.market_id,
99
+ score,
100
+ label,
101
+ component_breakdown: {
102
+ price_variation_24h_pct: parseFloat((priceVariation24h * 100).toFixed(4)),
103
+ volume_ratio: parseFloat(volumeRatio.toFixed(4)),
104
+ spread_pct: parseFloat(spreadPct.toFixed(4)),
105
+ spread_baseline_pct: spreadBaseline,
106
+ price_score: priceScore,
107
+ volume_score: volumeScore,
108
+ spread_score: spreadScore,
109
+ },
110
+ data_timestamp: new Date().toISOString(),
111
+ disclaimer:
112
+ "Sentiment is derived from market microstructure data only. Not investment advice.",
113
+ };
114
+
115
+ return {
116
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
117
+ };
118
+ } catch (err) {
119
+ const msg =
120
+ err instanceof BudaApiError
121
+ ? { error: err.message, code: err.status, path: err.path }
122
+ : { error: String(err), code: "UNKNOWN" };
123
+ return {
124
+ content: [{ type: "text", text: JSON.stringify(msg) }],
125
+ isError: true,
126
+ };
127
+ }
128
+ }
129
+
130
+ export function register(server: McpServer, client: BudaClient, cache: MemoryCache): void {
131
+ server.tool(
132
+ toolSchema.name,
133
+ toolSchema.description,
134
+ {
135
+ market_id: z
136
+ .string()
137
+ .describe("Market ID (e.g. 'BTC-CLP', 'ETH-BTC', 'BTC-USDT')."),
138
+ },
139
+ (args) => handleMarketSentiment(args, client, cache),
140
+ );
141
+ }
@@ -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
  };