@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 { MemoryCache, CACHE_TTL } from "../cache.js";
5
+ import { validateCurrency } from "../validation.js";
5
6
  import type { AllTickersResponse } from "../types.js";
6
7
 
7
8
  export const toolSchema = {
@@ -24,6 +25,82 @@ export const toolSchema = {
24
25
  },
25
26
  };
26
27
 
28
+ export async function handleCompareMarkets(
29
+ args: { base_currency: string },
30
+ client: BudaClient,
31
+ cache: MemoryCache,
32
+ ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
33
+ const { base_currency } = args;
34
+
35
+ const currencyError = validateCurrency(base_currency);
36
+ if (currencyError) {
37
+ return {
38
+ content: [{ type: "text", text: JSON.stringify({ error: currencyError, code: "INVALID_CURRENCY" }) }],
39
+ isError: true,
40
+ };
41
+ }
42
+
43
+ try {
44
+ const base = base_currency.toUpperCase();
45
+ const data = await cache.getOrFetch<AllTickersResponse>(
46
+ "tickers:all",
47
+ CACHE_TTL.TICKER,
48
+ () => client.get<AllTickersResponse>("/tickers"),
49
+ );
50
+
51
+ const matching = data.tickers.filter((t) => {
52
+ const [tickerBase] = t.market_id.split("-");
53
+ return tickerBase === base;
54
+ });
55
+
56
+ if (matching.length === 0) {
57
+ return {
58
+ content: [
59
+ {
60
+ type: "text",
61
+ text: JSON.stringify({
62
+ error: `No markets found for base currency '${base}'.`,
63
+ code: "NOT_FOUND",
64
+ }),
65
+ },
66
+ ],
67
+ isError: true,
68
+ };
69
+ }
70
+
71
+ const result = {
72
+ base_currency: base,
73
+ markets: matching.map((t) => ({
74
+ market_id: t.market_id,
75
+ last_price: parseFloat(t.last_price[0]),
76
+ last_price_currency: t.last_price[1],
77
+ best_bid: t.max_bid ? parseFloat(t.max_bid[0]) : null,
78
+ best_ask: t.min_ask ? parseFloat(t.min_ask[0]) : null,
79
+ volume_24h: t.volume ? parseFloat(t.volume[0]) : null,
80
+ price_change_24h: t.price_variation_24h
81
+ ? parseFloat((parseFloat(t.price_variation_24h) * 100).toFixed(4))
82
+ : null,
83
+ price_change_7d: t.price_variation_7d
84
+ ? parseFloat((parseFloat(t.price_variation_7d) * 100).toFixed(4))
85
+ : null,
86
+ })),
87
+ };
88
+
89
+ return {
90
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
91
+ };
92
+ } catch (err) {
93
+ const msg =
94
+ err instanceof BudaApiError
95
+ ? { error: err.message, code: err.status }
96
+ : { error: String(err), code: "UNKNOWN" };
97
+ return {
98
+ content: [{ type: "text", text: JSON.stringify(msg) }],
99
+ isError: true,
100
+ };
101
+ }
102
+ }
103
+
27
104
  export function register(server: McpServer, client: BudaClient, cache: MemoryCache): void {
28
105
  server.tool(
29
106
  toolSchema.name,
@@ -35,66 +112,6 @@ export function register(server: McpServer, client: BudaClient, cache: MemoryCac
35
112
  "Base currency to compare across all available markets (e.g. 'BTC', 'ETH', 'XRP').",
36
113
  ),
37
114
  },
38
- async ({ base_currency }) => {
39
- try {
40
- const base = base_currency.toUpperCase();
41
- const data = await cache.getOrFetch<AllTickersResponse>(
42
- "tickers:all",
43
- CACHE_TTL.TICKER,
44
- () => client.get<AllTickersResponse>("/tickers"),
45
- );
46
-
47
- const matching = data.tickers.filter((t) => {
48
- const [tickerBase] = t.market_id.split("-");
49
- return tickerBase === base;
50
- });
51
-
52
- if (matching.length === 0) {
53
- return {
54
- content: [
55
- {
56
- type: "text",
57
- text: JSON.stringify({
58
- error: `No markets found for base currency '${base}'.`,
59
- code: "NOT_FOUND",
60
- }),
61
- },
62
- ],
63
- isError: true,
64
- };
65
- }
66
-
67
- const result = {
68
- base_currency: base,
69
- markets: matching.map((t) => ({
70
- market_id: t.market_id,
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,
76
- price_change_24h: t.price_variation_24h
77
- ? parseFloat((parseFloat(t.price_variation_24h) * 100).toFixed(4))
78
- : null,
79
- price_change_7d: t.price_variation_7d
80
- ? parseFloat((parseFloat(t.price_variation_7d) * 100).toFixed(4))
81
- : null,
82
- })),
83
- };
84
-
85
- return {
86
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
87
- };
88
- } catch (err) {
89
- const msg =
90
- err instanceof BudaApiError
91
- ? { error: err.message, code: err.status, path: err.path }
92
- : { error: String(err), code: "UNKNOWN" };
93
- return {
94
- content: [{ type: "text", text: JSON.stringify(msg) }],
95
- isError: true,
96
- };
97
- }
98
- },
115
+ (args) => handleCompareMarkets(args, client, cache),
99
116
  );
100
117
  }
@@ -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 { OrdersResponse, OrderResponse } from "../types.js";
6
7
 
7
8
  // ---- Module-level timer state (persists across HTTP requests / tool invocations) ----
@@ -23,7 +24,7 @@ async function cancelAllOrdersForMarket(marketId: string, client: BudaClient): P
23
24
  const orders = data.orders ?? [];
24
25
  await Promise.allSettled(
25
26
  orders.map((order) =>
26
- client.put<OrderResponse>(`/orders/${order.id}`, { state: "canceling" }),
27
+ client.put<OrderResponse>(`/orders/${order.id}`, { order: { state: "canceling" } }),
27
28
  ),
28
29
  );
29
30
  timers.delete(marketId);
@@ -125,6 +126,7 @@ type ScheduleArgs = {
125
126
  export async function handleScheduleCancelAll(
126
127
  args: ScheduleArgs,
127
128
  client: BudaClient,
129
+ transport: "http" | "stdio" = "stdio",
128
130
  ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
129
131
  const { market_id, ttl_seconds, confirmation_token } = args;
130
132
 
@@ -155,9 +157,25 @@ export async function handleScheduleCancelAll(
155
157
  };
156
158
  }
157
159
 
160
+ if (!Number.isInteger(ttl_seconds) || ttl_seconds < 10 || ttl_seconds > 300) {
161
+ return {
162
+ content: [
163
+ {
164
+ type: "text",
165
+ text: JSON.stringify({
166
+ error: "ttl_seconds must be an integer between 10 and 300.",
167
+ code: "VALIDATION_ERROR",
168
+ }),
169
+ },
170
+ ],
171
+ isError: true,
172
+ };
173
+ }
174
+
158
175
  const id = market_id.toLowerCase();
159
176
  const entry = armTimer(id, ttl_seconds, client);
160
177
 
178
+ logAudit({ ts: new Date().toISOString(), tool: "schedule_cancel_all", transport, args_summary: { market_id: market_id.toUpperCase(), ttl_seconds }, success: true });
161
179
  return {
162
180
  content: [
163
181
  {
@@ -269,7 +287,11 @@ export function handleDisarmCancelTimer(
269
287
 
270
288
  // ---- Registration ----
271
289
 
272
- export function register(server: McpServer, client: BudaClient): void {
290
+ export function register(
291
+ server: McpServer,
292
+ client: BudaClient,
293
+ transport: "stdio" | "http" = "stdio",
294
+ ): void {
273
295
  server.tool(
274
296
  toolSchema.name,
275
297
  toolSchema.description,
@@ -287,7 +309,24 @@ export function register(server: McpServer, client: BudaClient): void {
287
309
  .string()
288
310
  .describe("Must equal exactly 'CONFIRM' (case-sensitive) to arm the switch."),
289
311
  },
290
- (args) => handleScheduleCancelAll(args, client),
312
+ (args) => {
313
+ if (transport === "http") {
314
+ return Promise.resolve({
315
+ content: [{
316
+ type: "text" as const,
317
+ text: JSON.stringify({
318
+ error:
319
+ "schedule_cancel_all is not available on the HTTP transport. " +
320
+ "Timer state is lost on every server restart or deploy. " +
321
+ "Run buda-mcp in stdio mode on a persistent local process to use this feature.",
322
+ code: "TRANSPORT_NOT_SUPPORTED",
323
+ }),
324
+ }],
325
+ isError: true,
326
+ });
327
+ }
328
+ return handleScheduleCancelAll(args, client);
329
+ },
291
330
  );
292
331
 
293
332
  server.tool(
@@ -298,7 +337,23 @@ export function register(server: McpServer, client: BudaClient): void {
298
337
  .string()
299
338
  .describe("Market ID whose timer should be renewed (e.g. 'BTC-CLP')."),
300
339
  },
301
- (args) => handleRenewCancelTimer(args, client),
340
+ (args) => {
341
+ if (transport === "http") {
342
+ return Promise.resolve({
343
+ content: [{
344
+ type: "text" as const,
345
+ text: JSON.stringify({
346
+ error:
347
+ "renew_cancel_timer is not available on the HTTP transport. " +
348
+ "Timer state is process-local and only meaningful in stdio mode on a persistent local process.",
349
+ code: "TRANSPORT_NOT_SUPPORTED",
350
+ }),
351
+ }],
352
+ isError: true,
353
+ });
354
+ }
355
+ return Promise.resolve(handleRenewCancelTimer(args, client));
356
+ },
302
357
  );
303
358
 
304
359
  server.tool(
@@ -309,6 +364,22 @@ export function register(server: McpServer, client: BudaClient): void {
309
364
  .string()
310
365
  .describe("Market ID whose timer should be disarmed (e.g. 'BTC-CLP')."),
311
366
  },
312
- (args) => handleDisarmCancelTimer(args),
367
+ (args) => {
368
+ if (transport === "http") {
369
+ return Promise.resolve({
370
+ content: [{
371
+ type: "text" as const,
372
+ text: JSON.stringify({
373
+ error:
374
+ "disarm_cancel_timer is not available on the HTTP transport. " +
375
+ "Timer state is process-local and only meaningful in stdio mode on a persistent local process.",
376
+ code: "TRANSPORT_NOT_SUPPORTED",
377
+ }),
378
+ }],
379
+ isError: true,
380
+ });
381
+ }
382
+ return Promise.resolve(handleDisarmCancelTimer(args));
383
+ },
313
384
  );
314
385
  }
@@ -104,7 +104,7 @@ export async function handleGetDepositHistory(
104
104
  } catch (err) {
105
105
  const msg =
106
106
  err instanceof BudaApiError
107
- ? { error: err.message, code: err.status, path: err.path }
107
+ ? { error: err.message, code: err.status }
108
108
  : { error: String(err), code: "UNKNOWN" };
109
109
  return {
110
110
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -189,7 +189,7 @@ export async function handleCreateFiatDeposit(
189
189
  } catch (err) {
190
190
  const msg =
191
191
  err instanceof BudaApiError
192
- ? { error: err.message, code: err.status, path: err.path }
192
+ ? { error: err.message, code: err.status }
193
193
  : { error: String(err), code: "UNKNOWN" };
194
194
  return {
195
195
  content: [{ type: "text", text: JSON.stringify(msg) }],
package/src/tools/fees.ts CHANGED
@@ -69,7 +69,7 @@ export async function handleGetNetworkFees(
69
69
  } catch (err) {
70
70
  const msg =
71
71
  err instanceof BudaApiError
72
- ? { error: err.message, code: err.status, path: err.path }
72
+ ? { error: err.message, code: err.status }
73
73
  : { error: String(err), code: "UNKNOWN" };
74
74
  return {
75
75
  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 { flattenAmount } from "../utils.js";
5
+ import { logAudit } from "../audit.js";
5
6
  import type { LightningWithdrawalResponse, LightningInvoiceResponse } from "../types.js";
6
7
 
7
8
  export const lightningWithdrawalToolSchema = {
@@ -69,6 +70,7 @@ type CreateLightningInvoiceArgs = {
69
70
  export async function handleLightningWithdrawal(
70
71
  args: LightningWithdrawalArgs,
71
72
  client: BudaClient,
73
+ transport: "http" | "stdio" = "stdio",
72
74
  ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
73
75
  const { invoice, confirmation_token } = args;
74
76
 
@@ -91,6 +93,22 @@ export async function handleLightningWithdrawal(
91
93
  };
92
94
  }
93
95
 
96
+ const BOLT11_RE = /^ln(bc|tb|bcrt)\d*[munp]?1[a-z0-9]{20,}$/i;
97
+ if (!BOLT11_RE.test(invoice)) {
98
+ return {
99
+ content: [{
100
+ type: "text",
101
+ text: JSON.stringify({
102
+ error:
103
+ "Invalid Lightning invoice format. " +
104
+ "Expected a BOLT-11 string starting with 'lnbc', 'lntb', or 'lnbcrt'.",
105
+ code: "INVALID_INVOICE",
106
+ }),
107
+ }],
108
+ isError: true,
109
+ };
110
+ }
111
+
94
112
  try {
95
113
  const data = await client.post<LightningWithdrawalResponse>(
96
114
  `/reserves/ln-btc/withdrawals`,
@@ -101,10 +119,10 @@ export async function handleLightningWithdrawal(
101
119
  const amount = flattenAmount(lw.amount);
102
120
  const fee = flattenAmount(lw.fee);
103
121
 
104
- return {
122
+ const result = {
105
123
  content: [
106
124
  {
107
- type: "text",
125
+ type: "text" as const,
108
126
  text: JSON.stringify(
109
127
  {
110
128
  id: lw.id,
@@ -122,15 +140,16 @@ export async function handleLightningWithdrawal(
122
140
  },
123
141
  ],
124
142
  };
143
+ logAudit({ ts: new Date().toISOString(), tool: "lightning_withdrawal", transport, args_summary: {}, success: true });
144
+ return result;
125
145
  } catch (err) {
126
146
  const msg =
127
147
  err instanceof BudaApiError
128
- ? { error: err.message, code: err.status, path: err.path }
148
+ ? { error: err.message, code: err.status }
129
149
  : { error: String(err), code: "UNKNOWN" };
130
- return {
131
- content: [{ type: "text", text: JSON.stringify(msg) }],
132
- isError: true,
133
- };
150
+ const result = { content: [{ type: "text" as const, text: JSON.stringify(msg) }], isError: true as const };
151
+ logAudit({ ts: new Date().toISOString(), tool: "lightning_withdrawal", transport, args_summary: {}, success: false, error_code: msg.code });
152
+ return result;
134
153
  }
135
154
  }
136
155
 
@@ -176,7 +195,7 @@ export async function handleCreateLightningInvoice(
176
195
  } catch (err) {
177
196
  const msg =
178
197
  err instanceof BudaApiError
179
- ? { error: err.message, code: err.status, path: err.path }
198
+ ? { error: err.message, code: err.status }
180
199
  : { error: String(err), code: "UNKNOWN" };
181
200
  return {
182
201
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -192,7 +211,7 @@ export function register(server: McpServer, client: BudaClient): void {
192
211
  {
193
212
  invoice: z
194
213
  .string()
195
- .min(1)
214
+ .min(50)
196
215
  .describe("BOLT-11 Lightning invoice string (starts with 'lnbc', 'lntb', etc.)."),
197
216
  confirmation_token: z
198
217
  .string()
@@ -118,7 +118,7 @@ export async function handleMarketSentiment(
118
118
  } catch (err) {
119
119
  const msg =
120
120
  err instanceof BudaApiError
121
- ? { error: err.message, code: err.status, path: err.path }
121
+ ? { error: err.message, code: err.status }
122
122
  : { error: String(err), code: "UNKNOWN" };
123
123
  return {
124
124
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -101,7 +101,7 @@ export async function handleMarketSummary(
101
101
  } catch (err) {
102
102
  const msg =
103
103
  err instanceof BudaApiError
104
- ? { error: err.message, code: err.status, path: err.path }
104
+ ? { error: err.message, code: err.status }
105
105
  : { error: String(err), code: "UNKNOWN" };
106
106
  return {
107
107
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -68,7 +68,7 @@ export function register(server: McpServer, client: BudaClient, cache: MemoryCac
68
68
  } catch (err) {
69
69
  const msg =
70
70
  err instanceof BudaApiError
71
- ? { error: err.message, code: err.status, path: err.path }
71
+ ? { error: err.message, code: err.status }
72
72
  : { error: String(err), code: "UNKNOWN" };
73
73
  return {
74
74
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -88,7 +88,7 @@ export async function handleGetOrder(
88
88
  } catch (err) {
89
89
  const msg =
90
90
  err instanceof BudaApiError
91
- ? { error: err.message, code: err.status, path: err.path }
91
+ ? { error: err.message, code: err.status }
92
92
  : { error: String(err), code: "UNKNOWN" };
93
93
  return {
94
94
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -109,7 +109,7 @@ export async function handleGetOrderByClientId(
109
109
  } catch (err) {
110
110
  const msg =
111
111
  err instanceof BudaApiError
112
- ? { error: err.message, code: err.status, path: err.path }
112
+ ? { error: err.message, code: err.status }
113
113
  : { error: String(err), code: "UNKNOWN" };
114
114
  return {
115
115
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -83,7 +83,7 @@ export function register(server: McpServer, client: BudaClient, cache: MemoryCac
83
83
  } catch (err) {
84
84
  const msg =
85
85
  err instanceof BudaApiError
86
- ? { error: err.message, code: err.status, path: err.path }
86
+ ? { error: err.message, code: err.status }
87
87
  : { error: String(err), code: "UNKNOWN" };
88
88
  return {
89
89
  content: [{ type: "text", text: JSON.stringify(msg) }],
@@ -135,7 +135,7 @@ export function register(server: McpServer, client: BudaClient): void {
135
135
  } catch (err) {
136
136
  const msg =
137
137
  err instanceof BudaApiError
138
- ? { error: err.message, code: err.status, path: err.path }
138
+ ? { error: err.message, code: err.status }
139
139
  : { error: String(err), code: "UNKNOWN" };
140
140
  return {
141
141
  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 = {
@@ -91,6 +92,7 @@ type PlaceOrderArgs = {
91
92
  export async function handlePlaceOrder(
92
93
  args: PlaceOrderArgs,
93
94
  client: BudaClient,
95
+ transport: "http" | "stdio" = "stdio",
94
96
  ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
95
97
  const {
96
98
  market_id,
@@ -191,6 +193,38 @@ export async function handlePlaceOrder(
191
193
  };
192
194
  }
193
195
 
196
+ if (gtd_timestamp !== undefined) {
197
+ const ts = new Date(gtd_timestamp).getTime();
198
+ if (isNaN(ts)) {
199
+ return {
200
+ content: [
201
+ {
202
+ type: "text",
203
+ text: JSON.stringify({
204
+ error: "gtd_timestamp must be a valid ISO 8601 datetime string.",
205
+ code: "VALIDATION_ERROR",
206
+ }),
207
+ },
208
+ ],
209
+ isError: true,
210
+ };
211
+ }
212
+ if (ts <= Date.now()) {
213
+ return {
214
+ content: [
215
+ {
216
+ type: "text",
217
+ text: JSON.stringify({
218
+ error: "gtd_timestamp must be a future datetime.",
219
+ code: "VALIDATION_ERROR",
220
+ }),
221
+ },
222
+ ],
223
+ isError: true,
224
+ };
225
+ }
226
+ }
227
+
194
228
  let limitType = "gtc";
195
229
  if (ioc) limitType = "ioc";
196
230
  else if (fok) limitType = "fok";
@@ -211,18 +245,35 @@ export async function handlePlaceOrder(
211
245
  payload,
212
246
  );
213
247
 
214
- return {
215
- content: [{ type: "text", text: JSON.stringify(data.order, null, 2) }],
248
+ const result = {
249
+ content: [{ type: "text" as const, text: JSON.stringify(data.order, null, 2) }],
216
250
  };
251
+ logAudit({
252
+ ts: new Date().toISOString(),
253
+ tool: "place_order",
254
+ transport,
255
+ args_summary: { market_id, type, price_type, amount },
256
+ success: true,
257
+ });
258
+ return result;
217
259
  } catch (err) {
218
260
  const msg =
219
261
  err instanceof BudaApiError
220
- ? { error: err.message, code: err.status, path: err.path }
262
+ ? { error: err.message, code: err.status }
221
263
  : { error: String(err), code: "UNKNOWN" };
222
- return {
223
- content: [{ type: "text", text: JSON.stringify(msg) }],
264
+ const result = {
265
+ content: [{ type: "text" as const, text: JSON.stringify(msg) }],
224
266
  isError: true,
225
267
  };
268
+ logAudit({
269
+ ts: new Date().toISOString(),
270
+ tool: "place_order",
271
+ transport,
272
+ args_summary: { market_id, type, price_type, amount },
273
+ success: false,
274
+ error_code: msg.code,
275
+ });
276
+ return result;
226
277
  }
227
278
  }
228
279
 
@@ -108,7 +108,7 @@ export function register(server: McpServer, client: BudaClient, _cache: MemoryCa
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) }],
@@ -100,7 +100,7 @@ export async function handleGetRealQuotation(
100
100
  } catch (err) {
101
101
  const msg =
102
102
  err instanceof BudaApiError
103
- ? { error: err.message, code: err.status, path: err.path }
103
+ ? { error: err.message, code: err.status }
104
104
  : { error: String(err), code: "UNKNOWN" };
105
105
  return {
106
106
  content: [{ type: "text", text: JSON.stringify(msg) }],