@guiie/buda-mcp 1.5.0 → 1.5.2

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 (123) hide show
  1. package/.cursor/rules/marketplace-docs-sync.mdc +32 -0
  2. package/CHANGELOG.md +75 -0
  3. package/PUBLISH_CHECKLIST.md +48 -89
  4. package/README.md +446 -78
  5. package/dist/audit.d.ts +21 -0
  6. package/dist/audit.d.ts.map +1 -0
  7. package/dist/audit.js +14 -0
  8. package/dist/client.d.ts +1 -0
  9. package/dist/client.d.ts.map +1 -1
  10. package/dist/client.js +2 -1
  11. package/dist/http.js +65 -7
  12. package/dist/index.js +12 -3
  13. package/dist/tools/account.js +1 -1
  14. package/dist/tools/arbitrage.js +1 -1
  15. package/dist/tools/balance.js +1 -1
  16. package/dist/tools/balances.js +1 -1
  17. package/dist/tools/banks.js +1 -1
  18. package/dist/tools/batch_orders.d.ts +6 -1
  19. package/dist/tools/batch_orders.d.ts.map +1 -1
  20. package/dist/tools/batch_orders.js +47 -3
  21. package/dist/tools/cancel_all_orders.d.ts +1 -1
  22. package/dist/tools/cancel_all_orders.d.ts.map +1 -1
  23. package/dist/tools/cancel_all_orders.js +10 -13
  24. package/dist/tools/cancel_order.d.ts +1 -1
  25. package/dist/tools/cancel_order.d.ts.map +1 -1
  26. package/dist/tools/cancel_order.js +10 -10
  27. package/dist/tools/cancel_order_by_client_id.d.ts +1 -1
  28. package/dist/tools/cancel_order_by_client_id.d.ts.map +1 -1
  29. package/dist/tools/cancel_order_by_client_id.js +9 -9
  30. package/dist/tools/compare_markets.d.ts +9 -0
  31. package/dist/tools/compare_markets.d.ts.map +1 -1
  32. package/dist/tools/compare_markets.js +63 -53
  33. package/dist/tools/dead_mans_switch.d.ts +2 -2
  34. package/dist/tools/dead_mans_switch.d.ts.map +1 -1
  35. package/dist/tools/dead_mans_switch.js +68 -6
  36. package/dist/tools/deposits.js +2 -2
  37. package/dist/tools/fees.js +1 -1
  38. package/dist/tools/lightning.d.ts +1 -1
  39. package/dist/tools/lightning.d.ts.map +1 -1
  40. package/dist/tools/lightning.js +25 -9
  41. package/dist/tools/market_sentiment.js +1 -1
  42. package/dist/tools/market_summary.js +1 -1
  43. package/dist/tools/markets.js +1 -1
  44. package/dist/tools/order_lookup.js +2 -2
  45. package/dist/tools/orderbook.js +1 -1
  46. package/dist/tools/orders.js +1 -1
  47. package/dist/tools/place_order.d.ts +1 -1
  48. package/dist/tools/place_order.d.ts.map +1 -1
  49. package/dist/tools/place_order.js +53 -4
  50. package/dist/tools/price_history.js +1 -1
  51. package/dist/tools/quotation.js +1 -1
  52. package/dist/tools/receive_addresses.d.ts +6 -1
  53. package/dist/tools/receive_addresses.d.ts.map +1 -1
  54. package/dist/tools/receive_addresses.js +37 -13
  55. package/dist/tools/remittance_recipients.js +2 -2
  56. package/dist/tools/remittances.d.ts +7 -2
  57. package/dist/tools/remittances.d.ts.map +1 -1
  58. package/dist/tools/remittances.js +46 -23
  59. package/dist/tools/simulate_order.js +1 -1
  60. package/dist/tools/spread.js +1 -1
  61. package/dist/tools/technical_indicators.d.ts.map +1 -1
  62. package/dist/tools/technical_indicators.js +3 -2
  63. package/dist/tools/ticker.js +1 -1
  64. package/dist/tools/trades.js +1 -1
  65. package/dist/tools/volume.js +1 -1
  66. package/dist/tools/withdrawals.d.ts +1 -1
  67. package/dist/tools/withdrawals.d.ts.map +1 -1
  68. package/dist/tools/withdrawals.js +21 -11
  69. package/dist/utils.d.ts +10 -0
  70. package/dist/utils.d.ts.map +1 -1
  71. package/dist/utils.js +29 -1
  72. package/dist/validation.d.ts +6 -0
  73. package/dist/validation.d.ts.map +1 -1
  74. package/dist/validation.js +26 -0
  75. package/dist/version.d.ts.map +1 -1
  76. package/dist/version.js +8 -1
  77. package/marketplace/README.md +1 -1
  78. package/marketplace/claude-listing.md +75 -4
  79. package/marketplace/gemini-tools.json +325 -2
  80. package/marketplace/openapi.yaml +160 -1
  81. package/package.json +2 -1
  82. package/server.json +2 -2
  83. package/src/audit.ts +24 -0
  84. package/src/client.ts +3 -1
  85. package/src/http.ts +75 -7
  86. package/src/index.ts +10 -3
  87. package/src/tools/account.ts +1 -1
  88. package/src/tools/arbitrage.ts +1 -1
  89. package/src/tools/balance.ts +1 -1
  90. package/src/tools/balances.ts +1 -1
  91. package/src/tools/banks.ts +1 -1
  92. package/src/tools/batch_orders.ts +52 -2
  93. package/src/tools/cancel_all_orders.ts +10 -12
  94. package/src/tools/cancel_order.ts +10 -9
  95. package/src/tools/cancel_order_by_client_id.ts +9 -8
  96. package/src/tools/compare_markets.ts +78 -61
  97. package/src/tools/dead_mans_switch.ts +76 -5
  98. package/src/tools/deposits.ts +2 -2
  99. package/src/tools/fees.ts +1 -1
  100. package/src/tools/lightning.ts +28 -9
  101. package/src/tools/market_sentiment.ts +1 -1
  102. package/src/tools/market_summary.ts +1 -1
  103. package/src/tools/markets.ts +1 -1
  104. package/src/tools/order_lookup.ts +2 -2
  105. package/src/tools/orderbook.ts +1 -1
  106. package/src/tools/orders.ts +1 -1
  107. package/src/tools/place_order.ts +56 -5
  108. package/src/tools/price_history.ts +1 -1
  109. package/src/tools/quotation.ts +1 -1
  110. package/src/tools/receive_addresses.ts +40 -13
  111. package/src/tools/remittance_recipients.ts +2 -2
  112. package/src/tools/remittances.ts +49 -22
  113. package/src/tools/simulate_order.ts +1 -1
  114. package/src/tools/spread.ts +1 -1
  115. package/src/tools/technical_indicators.ts +3 -2
  116. package/src/tools/ticker.ts +1 -1
  117. package/src/tools/trades.ts +1 -1
  118. package/src/tools/volume.ts +1 -1
  119. package/src/tools/withdrawals.ts +22 -10
  120. package/src/utils.ts +36 -1
  121. package/src/validation.ts +29 -0
  122. package/src/version.ts +11 -3
  123. package/test/unit.ts +623 -22
@@ -2,6 +2,7 @@ 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 { validateCurrency } from "../validation.js";
5
+ import { logAudit } from "../audit.js";
5
6
  import type { ReceiveAddressesResponse, SingleReceiveAddressResponse, ReceiveAddress } from "../types.js";
6
7
 
7
8
  export const createReceiveAddressToolSchema = {
@@ -10,6 +11,7 @@ export const createReceiveAddressToolSchema = {
10
11
  "Generates a new receive address for a crypto currency. " +
11
12
  "Creates a new blockchain deposit address for the given currency. " +
12
13
  "Each call generates a distinct address. Not idempotent. " +
14
+ "IMPORTANT: Pass confirmation_token='CONFIRM' to execute. " +
13
15
  "Only applicable to crypto currencies (BTC, ETH, etc.). " +
14
16
  "Requires BUDA_API_KEY and BUDA_API_SECRET. " +
15
17
  "Example: 'Give me a fresh Bitcoin deposit address.'",
@@ -20,8 +22,12 @@ export const createReceiveAddressToolSchema = {
20
22
  type: "string",
21
23
  description: "Currency code (e.g. 'BTC', 'ETH').",
22
24
  },
25
+ confirmation_token: {
26
+ type: "string",
27
+ description: "Safety confirmation. Must equal exactly 'CONFIRM' (case-sensitive) to generate a new address.",
28
+ },
23
29
  },
24
- required: ["currency"],
30
+ required: ["currency", "confirmation_token"],
25
31
  },
26
32
  };
27
33
 
@@ -109,7 +115,7 @@ export async function handleListReceiveAddresses(
109
115
  } catch (err) {
110
116
  const msg =
111
117
  err instanceof BudaApiError
112
- ? { error: err.message, code: err.status, path: err.path }
118
+ ? { error: err.message, code: err.status }
113
119
  : { error: String(err), code: "UNKNOWN" };
114
120
  return {
115
121
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -142,7 +148,7 @@ export async function handleGetReceiveAddress(
142
148
  } catch (err) {
143
149
  const msg =
144
150
  err instanceof BudaApiError
145
- ? { error: err.message, code: err.status, path: err.path }
151
+ ? { error: err.message, code: err.status }
146
152
  : { error: String(err), code: "UNKNOWN" };
147
153
  return {
148
154
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -152,10 +158,29 @@ export async function handleGetReceiveAddress(
152
158
  }
153
159
 
154
160
  export async function handleCreateReceiveAddress(
155
- args: { currency: string },
161
+ args: { currency: string; confirmation_token: string },
156
162
  client: BudaClient,
163
+ transport: "http" | "stdio" = "stdio",
157
164
  ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
158
- const { currency } = args;
165
+ const { currency, confirmation_token } = args;
166
+
167
+ if (confirmation_token !== "CONFIRM") {
168
+ return {
169
+ content: [
170
+ {
171
+ type: "text",
172
+ text: JSON.stringify({
173
+ error:
174
+ "Address not generated. confirmation_token must equal 'CONFIRM' to execute. " +
175
+ "Each call creates a distinct address — review and set confirmation_token='CONFIRM' to proceed.",
176
+ code: "CONFIRMATION_REQUIRED",
177
+ preview: { currency },
178
+ }),
179
+ },
180
+ ],
181
+ isError: true,
182
+ };
183
+ }
159
184
 
160
185
  const validationError = validateCurrency(currency);
161
186
  if (validationError) {
@@ -170,18 +195,17 @@ export async function handleCreateReceiveAddress(
170
195
  `/currencies/${currency.toUpperCase()}/receive_addresses`,
171
196
  {},
172
197
  );
173
- return {
174
- content: [{ type: "text", text: JSON.stringify(normalizeAddress(data.receive_address), null, 2) }],
175
- };
198
+ const result = { content: [{ type: "text" as const, text: JSON.stringify(normalizeAddress(data.receive_address), null, 2) }] };
199
+ logAudit({ ts: new Date().toISOString(), tool: "create_receive_address", transport, args_summary: { currency }, success: true });
200
+ return result;
176
201
  } catch (err) {
177
202
  const msg =
178
203
  err instanceof BudaApiError
179
- ? { error: err.message, code: err.status, path: err.path }
204
+ ? { error: err.message, code: err.status }
180
205
  : { error: String(err), code: "UNKNOWN" };
181
- return {
182
- content: [{ type: "text", text: JSON.stringify(msg) }],
183
- isError: true,
184
- };
206
+ const result = { content: [{ type: "text" as const, text: JSON.stringify(msg) }], isError: true as const };
207
+ logAudit({ ts: new Date().toISOString(), tool: "create_receive_address", transport, args_summary: { currency }, success: false, error_code: msg.code });
208
+ return result;
185
209
  }
186
210
  }
187
211
 
@@ -210,6 +234,9 @@ export function register(server: McpServer, client: BudaClient): void {
210
234
  createReceiveAddressToolSchema.description,
211
235
  {
212
236
  currency: z.string().min(2).max(10).describe("Currency code (e.g. 'BTC', 'ETH')."),
237
+ confirmation_token: z
238
+ .string()
239
+ .describe("Safety confirmation. Must equal exactly 'CONFIRM' (case-sensitive) to generate a new address."),
213
240
  },
214
241
  (args) => handleCreateReceiveAddress(args, client),
215
242
  );
@@ -87,7 +87,7 @@ export async function handleListRemittanceRecipients(
87
87
  } catch (err) {
88
88
  const msg =
89
89
  err instanceof BudaApiError
90
- ? { error: err.message, code: err.status, path: err.path }
90
+ ? { error: err.message, code: err.status }
91
91
  : { error: String(err), code: "UNKNOWN" };
92
92
  return {
93
93
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -108,7 +108,7 @@ export async function handleGetRemittanceRecipient(
108
108
  } catch (err) {
109
109
  const msg =
110
110
  err instanceof BudaApiError
111
- ? { error: err.message, code: err.status, path: err.path }
111
+ ? { error: err.message, code: err.status }
112
112
  : { error: String(err), code: "UNKNOWN" };
113
113
  return {
114
114
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -3,6 +3,7 @@ import { z } from "zod";
3
3
  import { BudaClient, BudaApiError } from "../client.js";
4
4
  import { flattenAmount } from "../utils.js";
5
5
  import { validateCurrency } from "../validation.js";
6
+ import { logAudit } from "../audit.js";
6
7
  import type { RemittancesResponse, SingleRemittanceResponse, Remittance } from "../types.js";
7
8
 
8
9
  export const listRemittancesToolSchema = {
@@ -34,7 +35,8 @@ export const quoteRemittanceToolSchema = {
34
35
  "Requests a price quote for a fiat remittance to a saved recipient. " +
35
36
  "Returns a remittance object in 'quoted' state with an expiry timestamp. " +
36
37
  "NOT idempotent — creates a new remittance record each call. " +
37
- "To execute, call accept_remittance_quote with the returned ID before it expires. " +
38
+ "IMPORTANT: Pass confirmation_token='CONFIRM' to execute. " +
39
+ "To execute the transfer, call accept_remittance_quote with the returned ID before it expires. " +
38
40
  "Requires BUDA_API_KEY and BUDA_API_SECRET. " +
39
41
  "Example: 'Get a remittance quote to send 100000 CLP to recipient 5.'",
40
42
  inputSchema: {
@@ -52,8 +54,12 @@ export const quoteRemittanceToolSchema = {
52
54
  type: "number",
53
55
  description: "ID of the saved remittance recipient.",
54
56
  },
57
+ confirmation_token: {
58
+ type: "string",
59
+ description: "Safety confirmation. Must equal exactly 'CONFIRM' (case-sensitive) to create the quote.",
60
+ },
55
61
  },
56
- required: ["currency", "amount", "recipient_id"],
62
+ required: ["currency", "amount", "recipient_id", "confirmation_token"],
57
63
  },
58
64
  };
59
65
 
@@ -147,7 +153,7 @@ export async function handleListRemittances(
147
153
  } catch (err) {
148
154
  const msg =
149
155
  err instanceof BudaApiError
150
- ? { error: err.message, code: err.status, path: err.path }
156
+ ? { error: err.message, code: err.status }
151
157
  : { error: String(err), code: "UNKNOWN" };
152
158
  return {
153
159
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -168,7 +174,7 @@ export async function handleGetRemittance(
168
174
  } catch (err) {
169
175
  const msg =
170
176
  err instanceof BudaApiError
171
- ? { error: err.message, code: err.status, path: err.path }
177
+ ? { error: err.message, code: err.status }
172
178
  : { error: String(err), code: "UNKNOWN" };
173
179
  return {
174
180
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -178,10 +184,29 @@ export async function handleGetRemittance(
178
184
  }
179
185
 
180
186
  export async function handleQuoteRemittance(
181
- args: { currency: string; amount: number; recipient_id: number },
187
+ args: { currency: string; amount: number; recipient_id: number; confirmation_token: string },
182
188
  client: BudaClient,
189
+ transport: "http" | "stdio" = "stdio",
183
190
  ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
184
- const { currency, amount, recipient_id } = args;
191
+ const { currency, amount, recipient_id, confirmation_token } = args;
192
+
193
+ if (confirmation_token !== "CONFIRM") {
194
+ return {
195
+ content: [
196
+ {
197
+ type: "text",
198
+ text: JSON.stringify({
199
+ error:
200
+ "Remittance quote not created. confirmation_token must equal 'CONFIRM' to execute. " +
201
+ "Review the details and set confirmation_token='CONFIRM' to proceed.",
202
+ code: "CONFIRMATION_REQUIRED",
203
+ preview: { currency, amount, recipient_id },
204
+ }),
205
+ },
206
+ ],
207
+ isError: true,
208
+ };
209
+ }
185
210
 
186
211
  const validationError = validateCurrency(currency);
187
212
  if (validationError) {
@@ -199,24 +224,24 @@ export async function handleQuoteRemittance(
199
224
  recipient_id,
200
225
  },
201
226
  });
202
- return {
203
- content: [{ type: "text", text: JSON.stringify(normalizeRemittance(data.remittance), null, 2) }],
204
- };
227
+ const result = { content: [{ type: "text" as const, text: JSON.stringify(normalizeRemittance(data.remittance), null, 2) }] };
228
+ logAudit({ ts: new Date().toISOString(), tool: "quote_remittance", transport, args_summary: { currency, amount, recipient_id }, success: true });
229
+ return result;
205
230
  } catch (err) {
206
231
  const msg =
207
232
  err instanceof BudaApiError
208
- ? { error: err.message, code: err.status, path: err.path }
233
+ ? { error: err.message, code: err.status }
209
234
  : { error: String(err), code: "UNKNOWN" };
210
- return {
211
- content: [{ type: "text", text: JSON.stringify(msg) }],
212
- isError: true,
213
- };
235
+ const result = { content: [{ type: "text" as const, text: JSON.stringify(msg) }], isError: true as const };
236
+ logAudit({ ts: new Date().toISOString(), tool: "quote_remittance", transport, args_summary: { currency, amount, recipient_id }, success: false, error_code: msg.code });
237
+ return result;
214
238
  }
215
239
  }
216
240
 
217
241
  export async function handleAcceptRemittanceQuote(
218
242
  args: { id: number; confirmation_token: string },
219
243
  client: BudaClient,
244
+ transport: "http" | "stdio" = "stdio",
220
245
  ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
221
246
  const { id, confirmation_token } = args;
222
247
 
@@ -241,18 +266,17 @@ export async function handleAcceptRemittanceQuote(
241
266
  const data = await client.put<SingleRemittanceResponse>(`/remittances/${id}`, {
242
267
  remittance: { state: "confirming" },
243
268
  });
244
- return {
245
- content: [{ type: "text", text: JSON.stringify(normalizeRemittance(data.remittance), null, 2) }],
246
- };
269
+ const result = { content: [{ type: "text" as const, text: JSON.stringify(normalizeRemittance(data.remittance), null, 2) }] };
270
+ logAudit({ ts: new Date().toISOString(), tool: "accept_remittance_quote", transport, args_summary: { remittance_id: id }, success: true });
271
+ return result;
247
272
  } catch (err) {
248
273
  const msg =
249
274
  err instanceof BudaApiError
250
- ? { error: err.message, code: err.status, path: err.path }
275
+ ? { error: err.message, code: err.status }
251
276
  : { error: String(err), code: "UNKNOWN" };
252
- return {
253
- content: [{ type: "text", text: JSON.stringify(msg) }],
254
- isError: true,
255
- };
277
+ const result = { content: [{ type: "text" as const, text: JSON.stringify(msg) }], isError: true as const };
278
+ logAudit({ ts: new Date().toISOString(), tool: "accept_remittance_quote", transport, args_summary: { remittance_id: id }, success: false, error_code: msg.code });
279
+ return result;
256
280
  }
257
281
  }
258
282
 
@@ -283,6 +307,9 @@ export function register(server: McpServer, client: BudaClient): void {
283
307
  currency: z.string().min(2).max(10).describe("Fiat currency code (e.g. 'CLP', 'COP')."),
284
308
  amount: z.number().positive().describe("Amount to remit (positive number)."),
285
309
  recipient_id: z.number().int().positive().describe("ID of the saved remittance recipient."),
310
+ confirmation_token: z
311
+ .string()
312
+ .describe("Safety confirmation. Must equal exactly 'CONFIRM' (case-sensitive) to create the quote."),
286
313
  },
287
314
  (args) => handleQuoteRemittance(args, client),
288
315
  );
@@ -148,7 +148,7 @@ export async function handleSimulateOrder(
148
148
  } catch (err) {
149
149
  const msg =
150
150
  err instanceof BudaApiError
151
- ? { error: err.message, code: err.status, path: err.path }
151
+ ? { error: err.message, code: err.status }
152
152
  : { error: String(err), code: "UNKNOWN" };
153
153
  return {
154
154
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -86,7 +86,7 @@ export function register(server: McpServer, client: BudaClient, cache: MemoryCac
86
86
  } catch (err) {
87
87
  const msg =
88
88
  err instanceof BudaApiError
89
- ? { error: err.message, code: err.status, path: err.path }
89
+ ? { error: err.message, code: err.status }
90
90
  : { error: String(err), code: "UNKNOWN" };
91
91
  return {
92
92
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -191,7 +191,7 @@ export async function handleTechnicalIndicators(
191
191
  const macdResult = macd(closes, 12, 26, 9);
192
192
  const bbResult = bollingerBands(closes, 20, 2);
193
193
  const sma20 = parseFloat(sma(closes, 20).toFixed(2));
194
- const sma50 = parseFloat(sma(closes, 50).toFixed(2));
194
+ const sma50 = closes.length >= 50 ? parseFloat(sma(closes, 50).toFixed(2)) : null;
195
195
  const lastClose = closes[closes.length - 1];
196
196
 
197
197
  // Signal interpretations
@@ -230,6 +230,7 @@ export async function handleTechnicalIndicators(
230
230
  bollinger_bands: bbResult,
231
231
  sma_20: sma20,
232
232
  sma_50: sma50,
233
+ sma_50_warning: sma50 === null ? `insufficient data (need 50 candles, have ${closes.length})` : undefined,
233
234
  },
234
235
  signals: {
235
236
  rsi_signal: rsiSignal,
@@ -246,7 +247,7 @@ export async function handleTechnicalIndicators(
246
247
  } catch (err) {
247
248
  const msg =
248
249
  err instanceof BudaApiError
249
- ? { error: err.message, code: err.status, path: err.path }
250
+ ? { error: err.message, code: err.status }
250
251
  : { error: String(err), code: "UNKNOWN" };
251
252
  return {
252
253
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -77,7 +77,7 @@ export function register(server: McpServer, client: BudaClient, cache: MemoryCac
77
77
  } catch (err) {
78
78
  const msg =
79
79
  err instanceof BudaApiError
80
- ? { error: err.message, code: err.status, path: err.path }
80
+ ? { error: err.message, code: err.status }
81
81
  : { error: String(err), code: "UNKNOWN" };
82
82
  return {
83
83
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -94,7 +94,7 @@ export function register(server: McpServer, client: BudaClient, _cache: MemoryCa
94
94
  } catch (err) {
95
95
  const msg =
96
96
  err instanceof BudaApiError
97
- ? { error: err.message, code: err.status, path: err.path }
97
+ ? { error: err.message, code: err.status }
98
98
  : { error: String(err), code: "UNKNOWN" };
99
99
  return {
100
100
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -71,7 +71,7 @@ export function register(server: McpServer, client: BudaClient, _cache: MemoryCa
71
71
  } catch (err) {
72
72
  const msg =
73
73
  err instanceof BudaApiError
74
- ? { error: err.message, code: err.status, path: err.path }
74
+ ? { error: err.message, code: err.status }
75
75
  : { error: String(err), code: "UNKNOWN" };
76
76
  return {
77
77
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -1,8 +1,9 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
3
  import { BudaClient, BudaApiError } from "../client.js";
4
- import { validateCurrency } from "../validation.js";
4
+ import { validateCurrency, validateCryptoAddress } from "../validation.js";
5
5
  import { flattenAmount } from "../utils.js";
6
+ import { logAudit } from "../audit.js";
6
7
  import type { WithdrawalsResponse, SingleWithdrawalResponse, Withdrawal } from "../types.js";
7
8
 
8
9
  export const getWithdrawalHistoryToolSchema = {
@@ -106,7 +107,7 @@ export async function handleGetWithdrawalHistory(
106
107
  } catch (err) {
107
108
  const msg =
108
109
  err instanceof BudaApiError
109
- ? { error: err.message, code: err.status, path: err.path }
110
+ ? { error: err.message, code: err.status }
110
111
  : { error: String(err), code: "UNKNOWN" };
111
112
  return {
112
113
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -120,6 +121,7 @@ export const createWithdrawalToolSchema = {
120
121
  description:
121
122
  "Create a withdrawal on Buda.com. Supports both crypto (address) and fiat (bank_account_id) withdrawals. " +
122
123
  "Exactly one of address or bank_account_id must be provided. " +
124
+ "WARNING: Crypto withdrawals are irreversible — verify the destination address carefully before confirming. " +
123
125
  "IMPORTANT: Pass confirmation_token='CONFIRM' to execute. " +
124
126
  "Requires BUDA_API_KEY and BUDA_API_SECRET.",
125
127
  inputSchema: {
@@ -151,6 +153,7 @@ type CreateWithdrawalArgs = {
151
153
  export async function handleCreateWithdrawal(
152
154
  args: CreateWithdrawalArgs,
153
155
  client: BudaClient,
156
+ transport: "http" | "stdio" = "stdio",
154
157
  ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
155
158
  const { currency, amount, address, network, bank_account_id, confirmation_token } = args;
156
159
 
@@ -213,6 +216,16 @@ export async function handleCreateWithdrawal(
213
216
  };
214
217
  }
215
218
 
219
+ if (hasAddress) {
220
+ const addrError = validateCryptoAddress(address!, currency);
221
+ if (addrError) {
222
+ return {
223
+ content: [{ type: "text", text: JSON.stringify({ error: addrError, code: "INVALID_ADDRESS" }) }],
224
+ isError: true,
225
+ };
226
+ }
227
+ }
228
+
216
229
  try {
217
230
  const payload: Record<string, unknown> = { amount: String(amount) };
218
231
  if (hasAddress) {
@@ -227,18 +240,17 @@ export async function handleCreateWithdrawal(
227
240
  payload,
228
241
  );
229
242
 
230
- return {
231
- content: [{ type: "text", text: JSON.stringify(normalizeWithdrawal(data.withdrawal), null, 2) }],
232
- };
243
+ const result = { content: [{ type: "text" as const, text: JSON.stringify(normalizeWithdrawal(data.withdrawal), null, 2) }] };
244
+ logAudit({ ts: new Date().toISOString(), tool: "create_withdrawal", transport, args_summary: { currency, amount, type: hasAddress ? "crypto" : "fiat" }, success: true });
245
+ return result;
233
246
  } catch (err) {
234
247
  const msg =
235
248
  err instanceof BudaApiError
236
- ? { error: err.message, code: err.status, path: err.path }
249
+ ? { error: err.message, code: err.status }
237
250
  : { error: String(err), code: "UNKNOWN" };
238
- return {
239
- content: [{ type: "text", text: JSON.stringify(msg) }],
240
- isError: true,
241
- };
251
+ const result = { content: [{ type: "text" as const, text: JSON.stringify(msg) }], isError: true as const };
252
+ logAudit({ ts: new Date().toISOString(), tool: "create_withdrawal", transport, args_summary: { currency, amount, type: hasAddress ? "crypto" : "fiat" }, success: false, error_code: msg.code });
253
+ return result;
242
254
  }
243
255
  }
244
256
 
package/src/utils.ts CHANGED
@@ -1,11 +1,46 @@
1
+ import { timingSafeEqual } from "crypto";
1
2
  import type { Amount, OhlcvCandle } from "./types.js";
2
3
 
4
+ /**
5
+ * Constant-time string comparison to prevent timing attacks on bearer tokens.
6
+ */
7
+ export function safeTokenEqual(a: string, b: string): boolean {
8
+ const aBuf = Buffer.from(a);
9
+ const bBuf = Buffer.from(b);
10
+ if (aBuf.length !== bBuf.length) return false;
11
+ return timingSafeEqual(aBuf, bBuf);
12
+ }
13
+
14
+ /**
15
+ * Parses a raw string (from an environment variable) as an integer within [min, max].
16
+ * Returns the fallback when raw is undefined.
17
+ * Throws a descriptive Error if the value is non-numeric or out of range.
18
+ */
19
+ export function parseEnvInt(
20
+ raw: string | undefined,
21
+ fallback: number,
22
+ min: number,
23
+ max: number,
24
+ name: string,
25
+ ): number {
26
+ if (raw === undefined) return fallback;
27
+ const n = parseInt(raw, 10);
28
+ if (isNaN(n) || n < min || n > max) {
29
+ throw new Error(
30
+ `[buda-mcp] Invalid ${name} "${raw}". Must be an integer between ${min} and ${max}.`,
31
+ );
32
+ }
33
+ return n;
34
+ }
35
+
3
36
  /**
4
37
  * Flattens a Buda API Amount tuple [value_string, currency] into a typed object.
5
38
  * All numeric strings are cast to float via parseFloat.
6
39
  */
7
40
  export function flattenAmount(amount: Amount): { value: number; currency: string } {
8
- return { value: parseFloat(amount[0]), currency: amount[1] };
41
+ const value = parseFloat(amount[0]);
42
+ if (isNaN(value)) throw new Error(`Invalid amount value: "${amount[0]}"`);
43
+ return { value, currency: amount[1] };
9
44
  }
10
45
 
11
46
  /**
package/src/validation.ts CHANGED
@@ -30,3 +30,32 @@ export function validateCurrency(id: string): string | null {
30
30
  }
31
31
  return null;
32
32
  }
33
+
34
+ // Per-currency address format rules.
35
+ // Unknown currencies pass through (undefined rule) — the exchange validates those.
36
+ const ADDRESS_RULES: Record<string, RegExp> = {
37
+ BTC: /^(bc1[a-z0-9]{6,87}|[13][a-zA-HJ-NP-Z0-9]{25,34})$/,
38
+ ETH: /^0x[0-9a-fA-F]{40}$/,
39
+ USDC: /^0x[0-9a-fA-F]{40}$/,
40
+ USDT: /^0x[0-9a-fA-F]{40}$/,
41
+ LTC: /^(ltc1[a-z0-9]{6,87}|[LM3][a-zA-HJ-NP-Z0-9]{25,34})$/,
42
+ BCH: /^(bitcoincash:)?[qp][a-z0-9]{41}$/,
43
+ XRP: /^r[1-9A-HJ-NP-Za-km-z]{24,33}$/,
44
+ };
45
+
46
+ /**
47
+ * Validates a crypto withdrawal address against known per-currency formats.
48
+ * Returns an error message string if the address is invalid, or null if valid
49
+ * (including null for unknown currencies, where the exchange is the last line of defence).
50
+ */
51
+ export function validateCryptoAddress(address: string, currency: string): string | null {
52
+ const rule = ADDRESS_RULES[currency.toUpperCase()];
53
+ if (!rule) return null;
54
+ if (!rule.test(address)) {
55
+ return (
56
+ `Invalid ${currency.toUpperCase()} address format. ` +
57
+ `Double-check the destination address — crypto withdrawals are irreversible.`
58
+ );
59
+ }
60
+ return null;
61
+ }
package/src/version.ts CHANGED
@@ -3,6 +3,14 @@ import { fileURLToPath } from "url";
3
3
  import { dirname, join } from "path";
4
4
 
5
5
  const _dir = dirname(fileURLToPath(import.meta.url));
6
- export const VERSION: string = (
7
- JSON.parse(readFileSync(join(_dir, "../package.json"), "utf8")) as { version: string }
8
- ).version;
6
+
7
+ let _version = "unknown";
8
+ try {
9
+ _version = (
10
+ JSON.parse(readFileSync(join(_dir, "../package.json"), "utf8")) as { version: string }
11
+ ).version;
12
+ } catch {
13
+ // package.json not found in deployment — use fallback
14
+ }
15
+
16
+ export const VERSION: string = _version;