@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
package/src/http.ts CHANGED
@@ -13,10 +13,18 @@ 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 * as simulateOrder from "./tools/simulate_order.js";
23
+ import * as positionSize from "./tools/calculate_position_size.js";
24
+ import * as marketSentiment from "./tools/market_sentiment.js";
25
+ import * as technicalIndicators from "./tools/technical_indicators.js";
26
+ import * as deadMansSwitch from "./tools/dead_mans_switch.js";
27
+ import { handleMarketSummary } from "./tools/market_summary.js";
20
28
 
21
29
  const PORT = parseInt(process.env.PORT ?? "3000", 10);
22
30
 
@@ -39,6 +47,12 @@ const PUBLIC_TOOL_SCHEMAS = [
39
47
  spread.toolSchema,
40
48
  compareMarkets.toolSchema,
41
49
  priceHistory.toolSchema,
50
+ arbitrage.toolSchema,
51
+ marketSummary.toolSchema,
52
+ simulateOrder.toolSchema,
53
+ positionSize.toolSchema,
54
+ marketSentiment.toolSchema,
55
+ technicalIndicators.toolSchema,
42
56
  ];
43
57
 
44
58
  const AUTH_TOOL_SCHEMAS = [
@@ -46,6 +60,9 @@ const AUTH_TOOL_SCHEMAS = [
46
60
  orders.toolSchema,
47
61
  placeOrder.toolSchema,
48
62
  cancelOrder.toolSchema,
63
+ deadMansSwitch.toolSchema,
64
+ deadMansSwitch.renewToolSchema,
65
+ deadMansSwitch.disarmToolSchema,
49
66
  ];
50
67
 
51
68
  function createServer(): McpServer {
@@ -62,12 +79,19 @@ function createServer(): McpServer {
62
79
  spread.register(server, client, reqCache);
63
80
  compareMarkets.register(server, client, reqCache);
64
81
  priceHistory.register(server, client, reqCache);
82
+ arbitrage.register(server, client, reqCache);
83
+ marketSummary.register(server, client, reqCache);
84
+ simulateOrder.register(server, client, reqCache);
85
+ positionSize.register(server);
86
+ marketSentiment.register(server, client, reqCache);
87
+ technicalIndicators.register(server, client);
65
88
 
66
89
  if (authEnabled) {
67
90
  balances.register(server, client);
68
91
  orders.register(server, client);
69
92
  placeOrder.register(server, client);
70
93
  cancelOrder.register(server, client);
94
+ deadMansSwitch.register(server, client);
71
95
  }
72
96
 
73
97
  // MCP Resources
@@ -114,6 +138,25 @@ function createServer(): McpServer {
114
138
  },
115
139
  );
116
140
 
141
+ server.resource(
142
+ "buda-summary",
143
+ new ResourceTemplate("buda://summary/{market}", { list: undefined }),
144
+ async (uri, params) => {
145
+ const marketId = (params.market as string).toUpperCase();
146
+ const result = await handleMarketSummary({ market_id: marketId }, client, reqCache);
147
+ const text = result.content[0].text;
148
+ return {
149
+ contents: [
150
+ {
151
+ uri: uri.href,
152
+ mimeType: "application/json",
153
+ text,
154
+ },
155
+ ],
156
+ };
157
+ },
158
+ );
159
+
117
160
  return server;
118
161
  }
119
162
 
@@ -140,6 +183,7 @@ app.get("/.well-known/mcp/server-card.json", (_req, res) => {
140
183
  resources: [
141
184
  { uri: "buda://markets", name: "All Buda.com markets", mimeType: "application/json" },
142
185
  { uri: "buda://ticker/{market}", name: "Ticker for a specific market", mimeType: "application/json" },
186
+ { uri: "buda://summary/{market}", name: "Full market summary with liquidity rating", mimeType: "application/json" },
143
187
  ],
144
188
  prompts: [],
145
189
  });
package/src/index.ts CHANGED
@@ -14,10 +14,18 @@ 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 * as simulateOrder from "./tools/simulate_order.js";
24
+ import * as positionSize from "./tools/calculate_position_size.js";
25
+ import * as marketSentiment from "./tools/market_sentiment.js";
26
+ import * as technicalIndicators from "./tools/technical_indicators.js";
27
+ import * as deadMansSwitch from "./tools/dead_mans_switch.js";
28
+ import { handleMarketSummary } from "./tools/market_summary.js";
21
29
 
22
30
  const client = new BudaClient(
23
31
  undefined,
@@ -39,6 +47,12 @@ volume.register(server, client, cache);
39
47
  spread.register(server, client, cache);
40
48
  compareMarkets.register(server, client, cache);
41
49
  priceHistory.register(server, client, cache);
50
+ arbitrage.register(server, client, cache);
51
+ marketSummary.register(server, client, cache);
52
+ simulateOrder.register(server, client, cache);
53
+ positionSize.register(server);
54
+ marketSentiment.register(server, client, cache);
55
+ technicalIndicators.register(server, client);
42
56
 
43
57
  // Auth-gated tools — only registered when API credentials are present
44
58
  if (client.hasAuth()) {
@@ -46,6 +60,7 @@ if (client.hasAuth()) {
46
60
  orders.register(server, client);
47
61
  placeOrder.register(server, client);
48
62
  cancelOrder.register(server, client);
63
+ deadMansSwitch.register(server, client);
49
64
  }
50
65
 
51
66
  // MCP Resources
@@ -92,5 +107,24 @@ server.resource(
92
107
  },
93
108
  );
94
109
 
110
+ server.resource(
111
+ "buda-summary",
112
+ new ResourceTemplate("buda://summary/{market}", { list: undefined }),
113
+ async (uri, params) => {
114
+ const marketId = (params.market as string).toUpperCase();
115
+ const result = await handleMarketSummary({ market_id: marketId }, client, cache);
116
+ const text = result.content[0].text;
117
+ return {
118
+ contents: [
119
+ {
120
+ uri: uri.href,
121
+ mimeType: "application/json",
122
+ text,
123
+ },
124
+ ],
125
+ };
126
+ },
127
+ );
128
+
95
129
  const transport = new StdioServerTransport();
96
130
  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 =
@@ -0,0 +1,141 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { validateMarketId } from "../validation.js";
4
+
5
+ export const toolSchema = {
6
+ name: "calculate_position_size",
7
+ description:
8
+ "Calculates position size based on your capital, risk tolerance, entry price, and stop-loss. " +
9
+ "Determines how many units to buy or sell so that a stop-loss hit costs exactly risk_pct% of capital. " +
10
+ "Fully client-side — no API call is made. " +
11
+ "Example: 'How many BTC can I buy on BTC-CLP if I have 1,000,000 CLP, risk 2%, entry 80,000,000 CLP, stop at 78,000,000 CLP?'",
12
+ inputSchema: {
13
+ type: "object" as const,
14
+ properties: {
15
+ market_id: {
16
+ type: "string",
17
+ description: "Market ID (e.g. 'BTC-CLP', 'ETH-COP'). Used to derive the quote currency.",
18
+ },
19
+ capital: {
20
+ type: "number",
21
+ description: "Total available capital in the quote currency (e.g. CLP for BTC-CLP).",
22
+ },
23
+ risk_pct: {
24
+ type: "number",
25
+ description: "Percentage of capital to risk on this trade (0.1–10, e.g. 2 = 2%).",
26
+ },
27
+ entry_price: {
28
+ type: "number",
29
+ description: "Planned entry price in quote currency.",
30
+ },
31
+ stop_loss_price: {
32
+ type: "number",
33
+ description:
34
+ "Stop-loss price in quote currency. Must be below entry for buys, above entry for sells.",
35
+ },
36
+ },
37
+ required: ["market_id", "capital", "risk_pct", "entry_price", "stop_loss_price"],
38
+ },
39
+ };
40
+
41
+ type CalculatePositionSizeArgs = {
42
+ market_id: string;
43
+ capital: number;
44
+ risk_pct: number;
45
+ entry_price: number;
46
+ stop_loss_price: number;
47
+ };
48
+
49
+ export function handleCalculatePositionSize(
50
+ args: CalculatePositionSizeArgs,
51
+ ): { content: Array<{ type: "text"; text: string }>; isError?: boolean } {
52
+ const { market_id, capital, risk_pct, entry_price, stop_loss_price } = args;
53
+
54
+ const validationError = validateMarketId(market_id);
55
+ if (validationError) {
56
+ return {
57
+ content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_MARKET_ID" }) }],
58
+ isError: true,
59
+ };
60
+ }
61
+
62
+ if (stop_loss_price === entry_price) {
63
+ return {
64
+ content: [
65
+ {
66
+ type: "text",
67
+ text: JSON.stringify({
68
+ error: "stop_loss_price must differ from entry_price.",
69
+ code: "INVALID_STOP_LOSS",
70
+ }),
71
+ },
72
+ ],
73
+ isError: true,
74
+ };
75
+ }
76
+
77
+ const quoteCurrency = market_id.split("-")[1].toUpperCase();
78
+ const baseCurrency = market_id.split("-")[0].toUpperCase();
79
+ const side: "buy" | "sell" = stop_loss_price < entry_price ? "buy" : "sell";
80
+
81
+ const capitalAtRisk = capital * (risk_pct / 100);
82
+ const riskPerUnit = Math.abs(entry_price - stop_loss_price);
83
+ const units = capitalAtRisk / riskPerUnit;
84
+ const positionValue = units * entry_price;
85
+ const feeImpact = parseFloat((positionValue * 0.008).toFixed(8));
86
+
87
+ const riskRewardNote =
88
+ `${side === "buy" ? "Buy" : "Sell"} ${units.toFixed(8)} ${baseCurrency} at ${entry_price} ${quoteCurrency} ` +
89
+ `with stop at ${stop_loss_price} ${quoteCurrency}. ` +
90
+ `Risking ${risk_pct}% of capital (${capitalAtRisk.toFixed(2)} ${quoteCurrency}) ` +
91
+ `on a ${riskPerUnit.toFixed(2)} ${quoteCurrency}/unit move. ` +
92
+ `Estimated entry fee: ${feeImpact.toFixed(2)} ${quoteCurrency} (0.8% taker, conservative estimate).`;
93
+
94
+ const result = {
95
+ market_id: market_id.toUpperCase(),
96
+ side,
97
+ units: parseFloat(units.toFixed(8)),
98
+ base_currency: baseCurrency,
99
+ capital_at_risk: parseFloat(capitalAtRisk.toFixed(2)),
100
+ position_value: parseFloat(positionValue.toFixed(2)),
101
+ fee_impact: feeImpact,
102
+ fee_currency: quoteCurrency,
103
+ risk_reward_note: riskRewardNote,
104
+ };
105
+
106
+ return {
107
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
108
+ };
109
+ }
110
+
111
+ export function register(server: McpServer): void {
112
+ server.tool(
113
+ toolSchema.name,
114
+ toolSchema.description,
115
+ {
116
+ market_id: z
117
+ .string()
118
+ .describe("Market ID (e.g. 'BTC-CLP', 'ETH-COP'). Used to derive the quote currency."),
119
+ capital: z
120
+ .number()
121
+ .positive()
122
+ .describe("Total available capital in the quote currency (e.g. CLP for BTC-CLP)."),
123
+ risk_pct: z
124
+ .number()
125
+ .min(0.1)
126
+ .max(10)
127
+ .describe("Percentage of capital to risk on this trade (0.1–10, e.g. 2 = 2%)."),
128
+ entry_price: z
129
+ .number()
130
+ .positive()
131
+ .describe("Planned entry price in quote currency."),
132
+ stop_loss_price: z
133
+ .number()
134
+ .positive()
135
+ .describe(
136
+ "Stop-loss price in quote currency. Must be below entry for buys, above entry for sells.",
137
+ ),
138
+ },
139
+ (args) => handleCalculatePositionSize(args),
140
+ );
141
+ }
@@ -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
  };