@guiie/buda-mcp 1.5.1 → 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 (103) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/PUBLISH_CHECKLIST.md +39 -32
  3. package/dist/audit.d.ts +21 -0
  4. package/dist/audit.d.ts.map +1 -0
  5. package/dist/audit.js +14 -0
  6. package/dist/http.js +25 -3
  7. package/dist/tools/account.js +1 -1
  8. package/dist/tools/arbitrage.js +1 -1
  9. package/dist/tools/balance.js +1 -1
  10. package/dist/tools/balances.js +1 -1
  11. package/dist/tools/banks.js +1 -1
  12. package/dist/tools/batch_orders.d.ts +1 -1
  13. package/dist/tools/batch_orders.d.ts.map +1 -1
  14. package/dist/tools/batch_orders.js +12 -2
  15. package/dist/tools/cancel_all_orders.d.ts +1 -1
  16. package/dist/tools/cancel_all_orders.d.ts.map +1 -1
  17. package/dist/tools/cancel_all_orders.js +10 -13
  18. package/dist/tools/cancel_order.d.ts +1 -1
  19. package/dist/tools/cancel_order.d.ts.map +1 -1
  20. package/dist/tools/cancel_order.js +9 -9
  21. package/dist/tools/cancel_order_by_client_id.d.ts +1 -1
  22. package/dist/tools/cancel_order_by_client_id.d.ts.map +1 -1
  23. package/dist/tools/cancel_order_by_client_id.js +9 -9
  24. package/dist/tools/compare_markets.d.ts +9 -0
  25. package/dist/tools/compare_markets.d.ts.map +1 -1
  26. package/dist/tools/compare_markets.js +63 -53
  27. package/dist/tools/dead_mans_switch.d.ts +1 -1
  28. package/dist/tools/dead_mans_switch.d.ts.map +1 -1
  29. package/dist/tools/dead_mans_switch.js +35 -3
  30. package/dist/tools/deposits.js +2 -2
  31. package/dist/tools/fees.js +1 -1
  32. package/dist/tools/lightning.d.ts +1 -1
  33. package/dist/tools/lightning.d.ts.map +1 -1
  34. package/dist/tools/lightning.js +11 -9
  35. package/dist/tools/market_sentiment.js +1 -1
  36. package/dist/tools/market_summary.js +1 -1
  37. package/dist/tools/markets.js +1 -1
  38. package/dist/tools/order_lookup.js +2 -2
  39. package/dist/tools/orderbook.js +1 -1
  40. package/dist/tools/orders.js +1 -1
  41. package/dist/tools/place_order.d.ts +1 -1
  42. package/dist/tools/place_order.d.ts.map +1 -1
  43. package/dist/tools/place_order.js +22 -4
  44. package/dist/tools/price_history.js +1 -1
  45. package/dist/tools/quotation.js +1 -1
  46. package/dist/tools/receive_addresses.d.ts +1 -1
  47. package/dist/tools/receive_addresses.d.ts.map +1 -1
  48. package/dist/tools/receive_addresses.js +11 -11
  49. package/dist/tools/remittance_recipients.js +2 -2
  50. package/dist/tools/remittances.d.ts +2 -2
  51. package/dist/tools/remittances.d.ts.map +1 -1
  52. package/dist/tools/remittances.js +19 -20
  53. package/dist/tools/simulate_order.js +1 -1
  54. package/dist/tools/spread.js +1 -1
  55. package/dist/tools/technical_indicators.js +1 -1
  56. package/dist/tools/ticker.js +1 -1
  57. package/dist/tools/trades.js +1 -1
  58. package/dist/tools/volume.js +1 -1
  59. package/dist/tools/withdrawals.d.ts +1 -1
  60. package/dist/tools/withdrawals.d.ts.map +1 -1
  61. package/dist/tools/withdrawals.js +10 -10
  62. package/dist/utils.d.ts +10 -0
  63. package/dist/utils.d.ts.map +1 -1
  64. package/dist/utils.js +25 -0
  65. package/package.json +1 -1
  66. package/server.json +2 -2
  67. package/src/audit.ts +24 -0
  68. package/src/http.ts +27 -3
  69. package/src/tools/account.ts +1 -1
  70. package/src/tools/arbitrage.ts +1 -1
  71. package/src/tools/balance.ts +1 -1
  72. package/src/tools/balances.ts +1 -1
  73. package/src/tools/banks.ts +1 -1
  74. package/src/tools/batch_orders.ts +12 -1
  75. package/src/tools/cancel_all_orders.ts +10 -12
  76. package/src/tools/cancel_order.ts +9 -8
  77. package/src/tools/cancel_order_by_client_id.ts +9 -8
  78. package/src/tools/compare_markets.ts +78 -61
  79. package/src/tools/dead_mans_switch.ts +37 -2
  80. package/src/tools/deposits.ts +2 -2
  81. package/src/tools/fees.ts +1 -1
  82. package/src/tools/lightning.ts +12 -9
  83. package/src/tools/market_sentiment.ts +1 -1
  84. package/src/tools/market_summary.ts +1 -1
  85. package/src/tools/markets.ts +1 -1
  86. package/src/tools/order_lookup.ts +2 -2
  87. package/src/tools/orderbook.ts +1 -1
  88. package/src/tools/orders.ts +1 -1
  89. package/src/tools/place_order.ts +24 -5
  90. package/src/tools/price_history.ts +1 -1
  91. package/src/tools/quotation.ts +1 -1
  92. package/src/tools/receive_addresses.ts +11 -10
  93. package/src/tools/remittance_recipients.ts +2 -2
  94. package/src/tools/remittances.ts +19 -18
  95. package/src/tools/simulate_order.ts +1 -1
  96. package/src/tools/spread.ts +1 -1
  97. package/src/tools/technical_indicators.ts +1 -1
  98. package/src/tools/ticker.ts +1 -1
  99. package/src/tools/trades.ts +1 -1
  100. package/src/tools/volume.ts +1 -1
  101. package/src/tools/withdrawals.ts +10 -9
  102. package/src/utils.ts +33 -0
  103. package/test/unit.ts +362 -4
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { BudaApiError } from "../client.js";
3
3
  import { flattenAmount } from "../utils.js";
4
4
  import { validateCurrency } from "../validation.js";
5
+ import { logAudit } from "../audit.js";
5
6
  export const listRemittancesToolSchema = {
6
7
  name: "list_remittances",
7
8
  description: "Returns all fiat remittance transfers for the authenticated Buda.com account. " +
@@ -130,7 +131,7 @@ export async function handleListRemittances(args, client) {
130
131
  }
131
132
  catch (err) {
132
133
  const msg = err instanceof BudaApiError
133
- ? { error: err.message, code: err.status, path: err.path }
134
+ ? { error: err.message, code: err.status }
134
135
  : { error: String(err), code: "UNKNOWN" };
135
136
  return {
136
137
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -147,7 +148,7 @@ export async function handleGetRemittance(args, client) {
147
148
  }
148
149
  catch (err) {
149
150
  const msg = err instanceof BudaApiError
150
- ? { error: err.message, code: err.status, path: err.path }
151
+ ? { error: err.message, code: err.status }
151
152
  : { error: String(err), code: "UNKNOWN" };
152
153
  return {
153
154
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -155,7 +156,7 @@ export async function handleGetRemittance(args, client) {
155
156
  };
156
157
  }
157
158
  }
158
- export async function handleQuoteRemittance(args, client) {
159
+ export async function handleQuoteRemittance(args, client, transport = "stdio") {
159
160
  const { currency, amount, recipient_id, confirmation_token } = args;
160
161
  if (confirmation_token !== "CONFIRM") {
161
162
  return {
@@ -188,21 +189,20 @@ export async function handleQuoteRemittance(args, client) {
188
189
  recipient_id,
189
190
  },
190
191
  });
191
- return {
192
- content: [{ type: "text", text: JSON.stringify(normalizeRemittance(data.remittance), null, 2) }],
193
- };
192
+ const result = { content: [{ type: "text", text: JSON.stringify(normalizeRemittance(data.remittance), null, 2) }] };
193
+ logAudit({ ts: new Date().toISOString(), tool: "quote_remittance", transport, args_summary: { currency, amount, recipient_id }, success: true });
194
+ return result;
194
195
  }
195
196
  catch (err) {
196
197
  const msg = err instanceof BudaApiError
197
- ? { error: err.message, code: err.status, path: err.path }
198
+ ? { error: err.message, code: err.status }
198
199
  : { error: String(err), code: "UNKNOWN" };
199
- return {
200
- content: [{ type: "text", text: JSON.stringify(msg) }],
201
- isError: true,
202
- };
200
+ const result = { content: [{ type: "text", text: JSON.stringify(msg) }], isError: true };
201
+ logAudit({ ts: new Date().toISOString(), tool: "quote_remittance", transport, args_summary: { currency, amount, recipient_id }, success: false, error_code: msg.code });
202
+ return result;
203
203
  }
204
204
  }
205
- export async function handleAcceptRemittanceQuote(args, client) {
205
+ export async function handleAcceptRemittanceQuote(args, client, transport = "stdio") {
206
206
  const { id, confirmation_token } = args;
207
207
  if (confirmation_token !== "CONFIRM") {
208
208
  return {
@@ -223,18 +223,17 @@ export async function handleAcceptRemittanceQuote(args, client) {
223
223
  const data = await client.put(`/remittances/${id}`, {
224
224
  remittance: { state: "confirming" },
225
225
  });
226
- return {
227
- content: [{ type: "text", text: JSON.stringify(normalizeRemittance(data.remittance), null, 2) }],
228
- };
226
+ const result = { content: [{ type: "text", text: JSON.stringify(normalizeRemittance(data.remittance), null, 2) }] };
227
+ logAudit({ ts: new Date().toISOString(), tool: "accept_remittance_quote", transport, args_summary: { remittance_id: id }, success: true });
228
+ return result;
229
229
  }
230
230
  catch (err) {
231
231
  const msg = err instanceof BudaApiError
232
- ? { error: err.message, code: err.status, path: err.path }
232
+ ? { error: err.message, code: err.status }
233
233
  : { error: String(err), code: "UNKNOWN" };
234
- return {
235
- content: [{ type: "text", text: JSON.stringify(msg) }],
236
- isError: true,
237
- };
234
+ const result = { content: [{ type: "text", text: JSON.stringify(msg) }], isError: true };
235
+ logAudit({ ts: new Date().toISOString(), tool: "accept_remittance_quote", transport, args_summary: { remittance_id: id }, success: false, error_code: msg.code });
236
+ return result;
238
237
  }
239
238
  }
240
239
  export function register(server, client) {
@@ -110,7 +110,7 @@ export async function handleSimulateOrder(args, client, cache) {
110
110
  }
111
111
  catch (err) {
112
112
  const msg = err instanceof BudaApiError
113
- ? { error: err.message, code: err.status, path: err.path }
113
+ ? { error: err.message, code: err.status }
114
114
  : { error: String(err), code: "UNKNOWN" };
115
115
  return {
116
116
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -67,7 +67,7 @@ export function register(server, client, cache) {
67
67
  }
68
68
  catch (err) {
69
69
  const msg = err instanceof BudaApiError
70
- ? { error: err.message, code: err.status, path: err.path }
70
+ ? { error: err.message, code: err.status }
71
71
  : { error: String(err), code: "UNKNOWN" };
72
72
  return {
73
73
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -195,7 +195,7 @@ export async function handleTechnicalIndicators(args, client) {
195
195
  }
196
196
  catch (err) {
197
197
  const msg = err instanceof BudaApiError
198
- ? { error: err.message, code: err.status, path: err.path }
198
+ ? { error: err.message, code: err.status }
199
199
  : { error: String(err), code: "UNKNOWN" };
200
200
  return {
201
201
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -60,7 +60,7 @@ export function register(server, client, cache) {
60
60
  }
61
61
  catch (err) {
62
62
  const msg = err instanceof BudaApiError
63
- ? { error: err.message, code: err.status, path: err.path }
63
+ ? { error: err.message, code: err.status }
64
64
  : { error: String(err), code: "UNKNOWN" };
65
65
  return {
66
66
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -76,7 +76,7 @@ export function register(server, client, _cache) {
76
76
  }
77
77
  catch (err) {
78
78
  const msg = err instanceof BudaApiError
79
- ? { error: err.message, code: err.status, path: err.path }
79
+ ? { error: err.message, code: err.status }
80
80
  : { error: String(err), code: "UNKNOWN" };
81
81
  return {
82
82
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -55,7 +55,7 @@ export function register(server, client, _cache) {
55
55
  }
56
56
  catch (err) {
57
57
  const msg = err instanceof BudaApiError
58
- ? { error: err.message, code: err.status, path: err.path }
58
+ ? { error: err.message, code: err.status }
59
59
  : { error: String(err), code: "UNKNOWN" };
60
60
  return {
61
61
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -81,7 +81,7 @@ type CreateWithdrawalArgs = {
81
81
  bank_account_id?: number;
82
82
  confirmation_token: string;
83
83
  };
84
- export declare function handleCreateWithdrawal(args: CreateWithdrawalArgs, client: BudaClient): Promise<{
84
+ export declare function handleCreateWithdrawal(args: CreateWithdrawalArgs, client: BudaClient, transport?: "http" | "stdio"): Promise<{
85
85
  content: Array<{
86
86
  type: "text";
87
87
  text: string;
@@ -1 +1 @@
1
- {"version":3,"file":"withdrawals.d.ts","sourceRoot":"","sources":["../../src/tools/withdrawals.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,UAAU,EAAgB,MAAM,cAAc,CAAC;AAKxD,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;CA8B1C,CAAC;AAEF,KAAK,wBAAwB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,mBAAmB,GAAG,SAAS,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,CAAC;IAC/E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAqBF,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,wBAAwB,EAC9B,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CA+ChF;AAED,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuBtC,CAAC;AAEF,KAAK,oBAAoB,GAAG;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kBAAkB,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,oBAAoB,EAC1B,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAmGhF;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CA+BpE"}
1
+ {"version":3,"file":"withdrawals.d.ts","sourceRoot":"","sources":["../../src/tools/withdrawals.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,UAAU,EAAgB,MAAM,cAAc,CAAC;AAMxD,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;CA8B1C,CAAC;AAEF,KAAK,wBAAwB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,mBAAmB,GAAG,SAAS,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,CAAC;IAC/E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAqBF,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,wBAAwB,EAC9B,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CA+ChF;AAED,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuBtC,CAAC;AAEF,KAAK,oBAAoB,GAAG;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kBAAkB,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,oBAAoB,EAC1B,MAAM,EAAE,UAAU,EAClB,SAAS,GAAE,MAAM,GAAG,OAAiB,GACpC,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAkGhF;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CA+BpE"}
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { BudaApiError } from "../client.js";
3
3
  import { validateCurrency, validateCryptoAddress } from "../validation.js";
4
4
  import { flattenAmount } from "../utils.js";
5
+ import { logAudit } from "../audit.js";
5
6
  export const getWithdrawalHistoryToolSchema = {
6
7
  name: "get_withdrawal_history",
7
8
  description: "Returns withdrawal history for a currency on the authenticated Buda.com account. " +
@@ -81,7 +82,7 @@ export async function handleGetWithdrawalHistory(args, client) {
81
82
  }
82
83
  catch (err) {
83
84
  const msg = err instanceof BudaApiError
84
- ? { error: err.message, code: err.status, path: err.path }
85
+ ? { error: err.message, code: err.status }
85
86
  : { error: String(err), code: "UNKNOWN" };
86
87
  return {
87
88
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -112,7 +113,7 @@ export const createWithdrawalToolSchema = {
112
113
  required: ["currency", "amount", "confirmation_token"],
113
114
  },
114
115
  };
115
- export async function handleCreateWithdrawal(args, client) {
116
+ export async function handleCreateWithdrawal(args, client, transport = "stdio") {
116
117
  const { currency, amount, address, network, bank_account_id, confirmation_token } = args;
117
118
  if (confirmation_token !== "CONFIRM") {
118
119
  return {
@@ -187,18 +188,17 @@ export async function handleCreateWithdrawal(args, client) {
187
188
  payload.bank_account_id = bank_account_id;
188
189
  }
189
190
  const data = await client.post(`/currencies/${currency.toUpperCase()}/withdrawals`, payload);
190
- return {
191
- content: [{ type: "text", text: JSON.stringify(normalizeWithdrawal(data.withdrawal), null, 2) }],
192
- };
191
+ const result = { content: [{ type: "text", text: JSON.stringify(normalizeWithdrawal(data.withdrawal), null, 2) }] };
192
+ logAudit({ ts: new Date().toISOString(), tool: "create_withdrawal", transport, args_summary: { currency, amount, type: hasAddress ? "crypto" : "fiat" }, success: true });
193
+ return result;
193
194
  }
194
195
  catch (err) {
195
196
  const msg = err instanceof BudaApiError
196
- ? { error: err.message, code: err.status, path: err.path }
197
+ ? { error: err.message, code: err.status }
197
198
  : { error: String(err), code: "UNKNOWN" };
198
- return {
199
- content: [{ type: "text", text: JSON.stringify(msg) }],
200
- isError: true,
201
- };
199
+ const result = { content: [{ type: "text", text: JSON.stringify(msg) }], isError: true };
200
+ logAudit({ ts: new Date().toISOString(), tool: "create_withdrawal", transport, args_summary: { currency, amount, type: hasAddress ? "crypto" : "fiat" }, success: false, error_code: msg.code });
201
+ return result;
202
202
  }
203
203
  }
204
204
  export function register(server, client) {
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,14 @@
1
1
  import type { Amount, OhlcvCandle } from "./types.js";
2
+ /**
3
+ * Constant-time string comparison to prevent timing attacks on bearer tokens.
4
+ */
5
+ export declare function safeTokenEqual(a: string, b: string): boolean;
6
+ /**
7
+ * Parses a raw string (from an environment variable) as an integer within [min, max].
8
+ * Returns the fallback when raw is undefined.
9
+ * Throws a descriptive Error if the value is non-numeric or out of range.
10
+ */
11
+ export declare function parseEnvInt(raw: string | undefined, fallback: number, min: number, max: number, name: string): number;
2
12
  /**
3
13
  * Flattens a Buda API Amount tuple [value_string, currency] into a typed object.
4
14
  * All numeric strings are cast to float via parseFloat.
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEtD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAIjF;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAI/E;AAWD;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,EAC3C,MAAM,EAAE,MAAM,GACb,WAAW,EAAE,CAoCf"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEtD;;GAEG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAK5D;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,GACX,MAAM,CASR;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAIjF;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAI/E;AAWD;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,EAC3C,MAAM,EAAE,MAAM,GACb,WAAW,EAAE,CAoCf"}
package/dist/utils.js CHANGED
@@ -1,3 +1,28 @@
1
+ import { timingSafeEqual } from "crypto";
2
+ /**
3
+ * Constant-time string comparison to prevent timing attacks on bearer tokens.
4
+ */
5
+ export function safeTokenEqual(a, b) {
6
+ const aBuf = Buffer.from(a);
7
+ const bBuf = Buffer.from(b);
8
+ if (aBuf.length !== bBuf.length)
9
+ return false;
10
+ return timingSafeEqual(aBuf, bBuf);
11
+ }
12
+ /**
13
+ * Parses a raw string (from an environment variable) as an integer within [min, max].
14
+ * Returns the fallback when raw is undefined.
15
+ * Throws a descriptive Error if the value is non-numeric or out of range.
16
+ */
17
+ export function parseEnvInt(raw, fallback, min, max, name) {
18
+ if (raw === undefined)
19
+ return fallback;
20
+ const n = parseInt(raw, 10);
21
+ if (isNaN(n) || n < min || n > max) {
22
+ throw new Error(`[buda-mcp] Invalid ${name} "${raw}". Must be an integer between ${min} and ${max}.`);
23
+ }
24
+ return n;
25
+ }
1
26
  /**
2
27
  * Flattens a Buda API Amount tuple [value_string, currency] into a typed object.
3
28
  * All numeric strings are cast to float via parseFloat.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guiie/buda-mcp",
3
- "version": "1.5.1",
3
+ "version": "1.5.2",
4
4
  "mcpName": "io.github.gtorreal/buda-mcp",
5
5
  "description": "MCP server for Buda.com's public cryptocurrency exchange API (Chile, Colombia, Peru)",
6
6
  "type": "module",
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/gtorreal/buda-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "1.5.1",
9
+ "version": "1.5.2",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@guiie/buda-mcp",
14
- "version": "1.5.1",
14
+ "version": "1.5.2",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }
package/src/audit.ts ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Structured audit logging for destructive MCP tool calls.
3
+ *
4
+ * Writes newline-delimited JSON to stderr so it never pollutes the stdio MCP transport
5
+ * and is captured by Railway / any log aggregator attached to the process.
6
+ *
7
+ * Rules for args_summary:
8
+ * - Include: market_id, currency, price_type, type, amount ranges
9
+ * - NEVER include: confirmation_token, invoice, address, bank_account_id
10
+ */
11
+
12
+ export interface AuditEvent {
13
+ ts: string;
14
+ tool: string;
15
+ transport: "http" | "stdio";
16
+ ip?: string;
17
+ args_summary: Record<string, unknown>;
18
+ success: boolean;
19
+ error_code?: string | number;
20
+ }
21
+
22
+ export function logAudit(event: AuditEvent): void {
23
+ process.stderr.write(JSON.stringify({ audit: true, ...event }) + "\n");
24
+ }
package/src/http.ts CHANGED
@@ -4,6 +4,7 @@ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mc
4
4
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5
5
  import { BudaClient } from "./client.js";
6
6
  import { MemoryCache, CACHE_TTL } from "./cache.js";
7
+ import { safeTokenEqual, parseEnvInt } from "./utils.js";
7
8
  import { VERSION } from "./version.js";
8
9
  import { validateMarketId } from "./validation.js";
9
10
  import type { MarketsResponse, TickerResponse } from "./types.js";
@@ -43,7 +44,13 @@ import * as batchOrders from "./tools/batch_orders.js";
43
44
  import * as lightning from "./tools/lightning.js";
44
45
  import { handleMarketSummary } from "./tools/market_summary.js";
45
46
 
46
- const PORT = parseInt(process.env.PORT ?? "3000", 10);
47
+ let PORT: number;
48
+ try {
49
+ PORT = parseEnvInt(process.env.PORT, 3000, 1, 65535, "PORT");
50
+ } catch (err) {
51
+ console.error(err instanceof Error ? err.message : String(err));
52
+ process.exit(1);
53
+ }
47
54
 
48
55
  const client = new BudaClient(
49
56
  undefined,
@@ -224,6 +231,9 @@ function createServer(): McpServer {
224
231
  }
225
232
 
226
233
  const app = express();
234
+ // Required for correct client IP detection behind Railway's reverse proxy.
235
+ // Without this, express-rate-limit sees the proxy IP instead of the real client.
236
+ app.set("trust proxy", 1);
227
237
  app.use(express.json());
228
238
 
229
239
  const MCP_AUTH_TOKEN = process.env.MCP_AUTH_TOKEN;
@@ -237,9 +247,23 @@ if (authEnabled && !MCP_AUTH_TOKEN) {
237
247
  process.exit(1);
238
248
  }
239
249
 
250
+ if (MCP_AUTH_TOKEN && MCP_AUTH_TOKEN.length < 32) {
251
+ console.warn(
252
+ "[buda-mcp] WARNING: MCP_AUTH_TOKEN has fewer than 32 characters. Use a longer random secret.",
253
+ );
254
+ }
255
+
256
+ let rateLimitMax: number;
257
+ try {
258
+ rateLimitMax = parseEnvInt(process.env.MCP_RATE_LIMIT, 120, 1, 10_000, "MCP_RATE_LIMIT");
259
+ } catch (err) {
260
+ console.error(err instanceof Error ? err.message : String(err));
261
+ process.exit(1);
262
+ }
263
+
240
264
  const mcpRateLimiter = rateLimit({
241
265
  windowMs: 60_000,
242
- max: parseInt(process.env.MCP_RATE_LIMIT ?? "120", 10),
266
+ max: rateLimitMax,
243
267
  standardHeaders: true,
244
268
  legacyHeaders: false,
245
269
  message: { error: "Too many requests. Retry after 60 seconds.", code: "RATE_LIMITED" },
@@ -255,7 +279,7 @@ function mcpAuthMiddleware(
255
279
  return;
256
280
  }
257
281
  const auth = req.headers.authorization ?? "";
258
- if (auth !== `Bearer ${MCP_AUTH_TOKEN}`) {
282
+ if (!safeTokenEqual(auth, `Bearer ${MCP_AUTH_TOKEN}`)) {
259
283
  res.status(401).json({ error: "Unauthorized" });
260
284
  return;
261
285
  }
@@ -47,7 +47,7 @@ export async function handleGetAccountInfo(
47
47
  } catch (err) {
48
48
  const msg =
49
49
  err instanceof BudaApiError
50
- ? { error: err.message, code: err.status, path: err.path }
50
+ ? { error: err.message, code: err.status }
51
51
  : { error: String(err), code: "UNKNOWN" };
52
52
  return {
53
53
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -171,7 +171,7 @@ export async function handleArbitrageOpportunities(
171
171
  } catch (err) {
172
172
  const msg =
173
173
  err instanceof BudaApiError
174
- ? { error: err.message, code: err.status, path: err.path }
174
+ ? { error: err.message, code: err.status }
175
175
  : { error: String(err), code: "UNKNOWN" };
176
176
  return {
177
177
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -73,7 +73,7 @@ export async function handleGetBalance(
73
73
  } catch (err) {
74
74
  const msg =
75
75
  err instanceof BudaApiError
76
- ? { error: err.message, code: err.status, path: err.path }
76
+ ? { error: err.message, code: err.status }
77
77
  : { error: String(err), code: "UNKNOWN" };
78
78
  return {
79
79
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -51,7 +51,7 @@ export function register(server: McpServer, client: BudaClient): void {
51
51
  } catch (err) {
52
52
  const msg =
53
53
  err instanceof BudaApiError
54
- ? { error: err.message, code: err.status, path: err.path }
54
+ ? { error: err.message, code: err.status }
55
55
  : { error: String(err), code: "UNKNOWN" };
56
56
  return {
57
57
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -73,7 +73,7 @@ export async function handleGetAvailableBanks(
73
73
  }
74
74
  const msg =
75
75
  err instanceof BudaApiError
76
- ? { error: err.message, code: err.status, path: err.path }
76
+ ? { error: err.message, code: err.status }
77
77
  : { error: String(err), code: "UNKNOWN" };
78
78
  return {
79
79
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -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 { validateMarketId } from "../validation.js";
5
+ import { logAudit } from "../audit.js";
5
6
  import type { OrderResponse } from "../types.js";
6
7
 
7
8
  export const toolSchema = {
@@ -76,6 +77,7 @@ type BatchOrdersArgs = {
76
77
  export async function handlePlaceBatchOrders(
77
78
  args: BatchOrdersArgs,
78
79
  client: BudaClient,
80
+ transport: "http" | "stdio" = "stdio",
79
81
  ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
80
82
  const { orders, max_notional, confirmation_token } = args;
81
83
 
@@ -202,9 +204,18 @@ export async function handlePlaceBatchOrders(
202
204
  response.warning = "Some orders failed. Already-placed orders were NOT rolled back.";
203
205
  }
204
206
 
207
+ const isError = failed > 0 && succeeded === 0 ? true : undefined;
208
+ logAudit({
209
+ ts: new Date().toISOString(),
210
+ tool: "place_batch_orders",
211
+ transport,
212
+ args_summary: { order_count: orders.length, succeeded, failed },
213
+ success: !isError,
214
+ error_code: isError ? "PARTIAL_OR_FULL_FAILURE" : undefined,
215
+ });
205
216
  return {
206
217
  content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
207
- isError: failed > 0 && succeeded === 0 ? true : undefined,
218
+ isError,
208
219
  };
209
220
  }
210
221
 
@@ -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 { validateMarketId } from "../validation.js";
5
+ import { logAudit } from "../audit.js";
5
6
  import type { CancelAllOrdersResponse } from "../types.js";
6
7
 
7
8
  export const toolSchema = {
@@ -37,6 +38,7 @@ type CancelAllOrdersArgs = {
37
38
  export async function handleCancelAllOrders(
38
39
  args: CancelAllOrdersArgs,
39
40
  client: BudaClient,
41
+ transport: "http" | "stdio" = "stdio",
40
42
  ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
41
43
  const { market_id, confirmation_token } = args;
42
44
 
@@ -76,23 +78,19 @@ export async function handleCancelAllOrders(
76
78
 
77
79
  const data = await client.delete<CancelAllOrdersResponse>(`/orders`, params);
78
80
 
79
- return {
80
- content: [
81
- {
82
- type: "text",
83
- text: JSON.stringify({ canceled_count: data.canceled_count, market_id }),
84
- },
85
- ],
81
+ const result = {
82
+ content: [{ type: "text" as const, text: JSON.stringify({ canceled_count: data.canceled_count, market_id }) }],
86
83
  };
84
+ logAudit({ ts: new Date().toISOString(), tool: "cancel_all_orders", transport, args_summary: { market_id }, success: true });
85
+ return result;
87
86
  } catch (err) {
88
87
  const msg =
89
88
  err instanceof BudaApiError
90
- ? { error: err.message, code: err.status, path: err.path }
89
+ ? { error: err.message, code: err.status }
91
90
  : { error: String(err), code: "UNKNOWN" };
92
- return {
93
- content: [{ type: "text", text: JSON.stringify(msg) }],
94
- isError: true,
95
- };
91
+ const result = { content: [{ type: "text" as const, text: JSON.stringify(msg) }], isError: true as const };
92
+ logAudit({ ts: new Date().toISOString(), tool: "cancel_all_orders", transport, args_summary: { market_id }, success: false, error_code: msg.code });
93
+ return result;
96
94
  }
97
95
  }
98
96
 
@@ -1,6 +1,7 @@
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 { logAudit } from "../audit.js";
4
5
  import type { OrderResponse } from "../types.js";
5
6
 
6
7
  export const toolSchema = {
@@ -36,6 +37,7 @@ type CancelOrderArgs = {
36
37
  export async function handleCancelOrder(
37
38
  args: CancelOrderArgs,
38
39
  client: BudaClient,
40
+ transport: "http" | "stdio" = "stdio",
39
41
  ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
40
42
  const { order_id, confirmation_token } = args;
41
43
 
@@ -62,18 +64,17 @@ export async function handleCancelOrder(
62
64
  order: { state: "canceling" },
63
65
  });
64
66
 
65
- return {
66
- content: [{ type: "text", text: JSON.stringify(data.order, null, 2) }],
67
- };
67
+ const result = { content: [{ type: "text" as const, text: JSON.stringify(data.order, null, 2) }] };
68
+ logAudit({ ts: new Date().toISOString(), tool: "cancel_order", transport, args_summary: { order_id }, success: true });
69
+ return result;
68
70
  } catch (err) {
69
71
  const msg =
70
72
  err instanceof BudaApiError
71
- ? { error: err.message, code: err.status, path: err.path }
73
+ ? { error: err.message, code: err.status }
72
74
  : { error: String(err), code: "UNKNOWN" };
73
- return {
74
- content: [{ type: "text", text: JSON.stringify(msg) }],
75
- isError: true,
76
- };
75
+ const result = { content: [{ type: "text" as const, text: JSON.stringify(msg) }], isError: true as const };
76
+ logAudit({ ts: new Date().toISOString(), tool: "cancel_order", transport, args_summary: { order_id }, success: false, error_code: msg.code });
77
+ return result;
77
78
  }
78
79
  }
79
80
 
@@ -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 { flattenAmount } from "../utils.js";
5
+ import { logAudit } from "../audit.js";
5
6
  import type { OrderResponse, Order } from "../types.js";
6
7
 
7
8
  export const toolSchema = {
@@ -69,6 +70,7 @@ function normalizeOrder(o: Order) {
69
70
  export async function handleCancelOrderByClientId(
70
71
  args: CancelOrderByClientIdArgs,
71
72
  client: BudaClient,
73
+ transport: "http" | "stdio" = "stdio",
72
74
  ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
73
75
  const { client_id, confirmation_token } = args;
74
76
 
@@ -96,18 +98,17 @@ export async function handleCancelOrderByClientId(
96
98
  { order: { state: "canceling" } },
97
99
  );
98
100
 
99
- return {
100
- content: [{ type: "text", text: JSON.stringify(normalizeOrder(data.order), null, 2) }],
101
- };
101
+ const result = { content: [{ type: "text" as const, text: JSON.stringify(normalizeOrder(data.order), null, 2) }] };
102
+ logAudit({ ts: new Date().toISOString(), tool: "cancel_order_by_client_id", transport, args_summary: {}, success: true });
103
+ return result;
102
104
  } catch (err) {
103
105
  const msg =
104
106
  err instanceof BudaApiError
105
- ? { error: err.message, code: err.status, path: err.path }
107
+ ? { error: err.message, code: err.status }
106
108
  : { error: String(err), code: "UNKNOWN" };
107
- return {
108
- content: [{ type: "text", text: JSON.stringify(msg) }],
109
- isError: true,
110
- };
109
+ const result = { content: [{ type: "text" as const, text: JSON.stringify(msg) }], isError: true as const };
110
+ logAudit({ ts: new Date().toISOString(), tool: "cancel_order_by_client_id", transport, args_summary: {}, success: false, error_code: msg.code });
111
+ return result;
111
112
  }
112
113
  }
113
114