@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
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.0",
9
+ "version": "1.5.2",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@guiie/buda-mcp",
14
- "version": "1.5.0",
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/client.ts CHANGED
@@ -34,8 +34,10 @@ export class BudaClient {
34
34
  return Boolean(this.apiKey && this.apiSecret);
35
35
  }
36
36
 
37
+ private _nonceCounter = 0;
38
+
37
39
  private nonce(): string {
38
- return String(Math.floor(Date.now() * 1000));
40
+ return String(Date.now() * 1000 + (this._nonceCounter++ % 1000));
39
41
  }
40
42
 
41
43
  private sign(method: string, pathWithQuery: string, body: string, nonce: string): string {
package/src/http.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  import express from "express";
2
+ import rateLimit from "express-rate-limit";
2
3
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
3
4
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
5
  import { BudaClient } from "./client.js";
5
6
  import { MemoryCache, CACHE_TTL } from "./cache.js";
7
+ import { safeTokenEqual, parseEnvInt } from "./utils.js";
6
8
  import { VERSION } from "./version.js";
9
+ import { validateMarketId } from "./validation.js";
7
10
  import type { MarketsResponse, TickerResponse } from "./types.js";
8
11
  import * as markets from "./tools/markets.js";
9
12
  import * as ticker from "./tools/ticker.js";
@@ -41,7 +44,13 @@ import * as batchOrders from "./tools/batch_orders.js";
41
44
  import * as lightning from "./tools/lightning.js";
42
45
  import { handleMarketSummary } from "./tools/market_summary.js";
43
46
 
44
- 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
+ }
45
54
 
46
55
  const client = new BudaClient(
47
56
  undefined,
@@ -133,7 +142,7 @@ function createServer(): McpServer {
133
142
  orders.register(server, client);
134
143
  placeOrder.register(server, client);
135
144
  cancelOrder.register(server, client);
136
- deadMansSwitch.register(server, client);
145
+ deadMansSwitch.register(server, client, "http");
137
146
  account.register(server, client);
138
147
  balance.register(server, client);
139
148
  orderLookup.register(server, client);
@@ -175,7 +184,10 @@ function createServer(): McpServer {
175
184
  "buda-ticker",
176
185
  new ResourceTemplate("buda://ticker/{market}", { list: undefined }),
177
186
  async (uri, params) => {
178
- const marketId = (params.market as string).toLowerCase();
187
+ const raw = params.market as string;
188
+ const validationError = validateMarketId(raw);
189
+ if (validationError) throw new Error(validationError);
190
+ const marketId = raw.toLowerCase();
179
191
  const data = await reqCache.getOrFetch<TickerResponse>(
180
192
  `ticker:${marketId}`,
181
193
  CACHE_TTL.TICKER,
@@ -197,9 +209,12 @@ function createServer(): McpServer {
197
209
  "buda-summary",
198
210
  new ResourceTemplate("buda://summary/{market}", { list: undefined }),
199
211
  async (uri, params) => {
200
- const marketId = (params.market as string).toUpperCase();
212
+ const raw = params.market as string;
213
+ const validationError = validateMarketId(raw);
214
+ if (validationError) throw new Error(validationError);
215
+ const marketId = raw.toUpperCase();
201
216
  const result = await handleMarketSummary({ market_id: marketId }, client, reqCache);
202
- const text = result.content[0].text;
217
+ const text = result.content[0]?.text ?? JSON.stringify({ error: "No content returned" });
203
218
  return {
204
219
  contents: [
205
220
  {
@@ -216,8 +231,61 @@ function createServer(): McpServer {
216
231
  }
217
232
 
218
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);
219
237
  app.use(express.json());
220
238
 
239
+ const MCP_AUTH_TOKEN = process.env.MCP_AUTH_TOKEN;
240
+
241
+ if (authEnabled && !MCP_AUTH_TOKEN) {
242
+ console.error(
243
+ "[buda-mcp] FATAL: BUDA_API_KEY/BUDA_API_SECRET are set but MCP_AUTH_TOKEN is not.\n" +
244
+ " The /mcp endpoint would be publicly accessible with full account access.\n" +
245
+ " Set MCP_AUTH_TOKEN to a long random secret, or run in stdio mode instead.",
246
+ );
247
+ process.exit(1);
248
+ }
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
+
264
+ const mcpRateLimiter = rateLimit({
265
+ windowMs: 60_000,
266
+ max: rateLimitMax,
267
+ standardHeaders: true,
268
+ legacyHeaders: false,
269
+ message: { error: "Too many requests. Retry after 60 seconds.", code: "RATE_LIMITED" },
270
+ });
271
+
272
+ function mcpAuthMiddleware(
273
+ req: express.Request,
274
+ res: express.Response,
275
+ next: express.NextFunction,
276
+ ): void {
277
+ if (!MCP_AUTH_TOKEN) {
278
+ next();
279
+ return;
280
+ }
281
+ const auth = req.headers.authorization ?? "";
282
+ if (!safeTokenEqual(auth, `Bearer ${MCP_AUTH_TOKEN}`)) {
283
+ res.status(401).json({ error: "Unauthorized" });
284
+ return;
285
+ }
286
+ next();
287
+ }
288
+
221
289
  // Health check for Railway / uptime monitors
222
290
  app.get("/health", (_req, res) => {
223
291
  res.json({
@@ -245,7 +313,7 @@ app.get("/.well-known/mcp/server-card.json", (_req, res) => {
245
313
  });
246
314
 
247
315
  // Stateless StreamableHTTP — new server instance per request (no session state needed)
248
- app.post("/mcp", async (req, res) => {
316
+ app.post("/mcp", mcpRateLimiter, mcpAuthMiddleware, async (req, res) => {
249
317
  const transport = new StreamableHTTPServerTransport({
250
318
  sessionIdGenerator: undefined,
251
319
  });
@@ -260,7 +328,7 @@ app.post("/mcp", async (req, res) => {
260
328
  });
261
329
 
262
330
  // SSE upgrade for clients that prefer streaming
263
- app.get("/mcp", async (req, res) => {
331
+ app.get("/mcp", mcpRateLimiter, mcpAuthMiddleware, async (req, res) => {
264
332
  const transport = new StreamableHTTPServerTransport({
265
333
  sessionIdGenerator: undefined,
266
334
  });
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { BudaClient } from "./client.js";
6
6
  import { cache, CACHE_TTL } from "./cache.js";
7
7
  import { VERSION } from "./version.js";
8
+ import { validateMarketId } from "./validation.js";
8
9
  import type { MarketsResponse, TickerResponse } from "./types.js";
9
10
  import * as markets from "./tools/markets.js";
10
11
  import * as ticker from "./tools/ticker.js";
@@ -119,7 +120,10 @@ server.resource(
119
120
  "buda-ticker",
120
121
  new ResourceTemplate("buda://ticker/{market}", { list: undefined }),
121
122
  async (uri, params) => {
122
- const marketId = (params.market as string).toLowerCase();
123
+ const raw = params.market as string;
124
+ const validationError = validateMarketId(raw);
125
+ if (validationError) throw new Error(validationError);
126
+ const marketId = raw.toLowerCase();
123
127
  const data = await cache.getOrFetch<TickerResponse>(
124
128
  `ticker:${marketId}`,
125
129
  CACHE_TTL.TICKER,
@@ -141,9 +145,12 @@ server.resource(
141
145
  "buda-summary",
142
146
  new ResourceTemplate("buda://summary/{market}", { list: undefined }),
143
147
  async (uri, params) => {
144
- const marketId = (params.market as string).toUpperCase();
148
+ const raw = params.market as string;
149
+ const validationError = validateMarketId(raw);
150
+ if (validationError) throw new Error(validationError);
151
+ const marketId = raw.toUpperCase();
145
152
  const result = await handleMarketSummary({ market_id: marketId }, client, cache);
146
- const text = result.content[0].text;
153
+ const text = result.content[0]?.text ?? JSON.stringify({ error: "No content returned" });
147
154
  return {
148
155
  contents: [
149
156
  {
@@ -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 = {
@@ -10,6 +11,7 @@ export const toolSchema = {
10
11
  "Place multiple orders sequentially on Buda.com (up to 20). " +
11
12
  "All orders are pre-validated before any API call — a validation failure stops execution with zero orders placed. " +
12
13
  "Partial API failures do NOT roll back already-placed orders. " +
14
+ "Use max_notional to cap total exposure (computed as sum of amount × limit_price for limit orders; market orders contribute 0). " +
13
15
  "IMPORTANT: Pass confirmation_token='CONFIRM' to execute. " +
14
16
  "Requires BUDA_API_KEY and BUDA_API_SECRET.",
15
17
  inputSchema: {
@@ -30,6 +32,13 @@ export const toolSchema = {
30
32
  required: ["market_id", "type", "price_type", "amount"],
31
33
  },
32
34
  },
35
+ max_notional: {
36
+ type: "number",
37
+ description:
38
+ "Optional spending cap: total notional (sum of amount × limit_price for limit orders). " +
39
+ "Batch is rejected before any API call if the sum exceeds this value. " +
40
+ "Market orders contribute 0 to the notional since their execution price is unknown.",
41
+ },
33
42
  confirmation_token: {
34
43
  type: "string",
35
44
  description:
@@ -61,14 +70,16 @@ type BatchResult = {
61
70
 
62
71
  type BatchOrdersArgs = {
63
72
  orders: SingleOrderInput[];
73
+ max_notional?: number;
64
74
  confirmation_token: string;
65
75
  };
66
76
 
67
77
  export async function handlePlaceBatchOrders(
68
78
  args: BatchOrdersArgs,
69
79
  client: BudaClient,
80
+ transport: "http" | "stdio" = "stdio",
70
81
  ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
71
- const { orders, confirmation_token } = args;
82
+ const { orders, max_notional, confirmation_token } = args;
72
83
 
73
84
  if (confirmation_token !== "CONFIRM") {
74
85
  return {
@@ -124,6 +135,27 @@ export async function handlePlaceBatchOrders(
124
135
  }
125
136
  }
126
137
 
138
+ // Notional cap check (limit orders only; market orders have unknown execution price)
139
+ if (max_notional !== undefined) {
140
+ const totalNotional = orders.reduce((sum, o) => {
141
+ return sum + (o.price_type === "limit" && o.limit_price ? o.amount * o.limit_price : 0);
142
+ }, 0);
143
+ if (totalNotional > max_notional) {
144
+ return {
145
+ content: [{
146
+ type: "text",
147
+ text: JSON.stringify({
148
+ error: `Total notional ${totalNotional} exceeds max_notional cap of ${max_notional}. No orders were placed.`,
149
+ code: "NOTIONAL_CAP_EXCEEDED",
150
+ total_notional: totalNotional,
151
+ max_notional,
152
+ }),
153
+ }],
154
+ isError: true,
155
+ };
156
+ }
157
+ }
158
+
127
159
  // Execute sequentially
128
160
  const results: BatchResult[] = [];
129
161
  for (let i = 0; i < orders.length; i++) {
@@ -172,9 +204,18 @@ export async function handlePlaceBatchOrders(
172
204
  response.warning = "Some orders failed. Already-placed orders were NOT rolled back.";
173
205
  }
174
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
+ });
175
216
  return {
176
217
  content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
177
- isError: failed > 0 && succeeded === 0 ? true : undefined,
218
+ isError,
178
219
  };
179
220
  }
180
221
 
@@ -188,6 +229,15 @@ export function register(server: McpServer, client: BudaClient): void {
188
229
  .min(1)
189
230
  .max(20)
190
231
  .describe("Array of 1–20 orders to place."),
232
+ max_notional: z
233
+ .number()
234
+ .positive()
235
+ .optional()
236
+ .describe(
237
+ "Optional spending cap: total notional (sum of amount × limit_price for limit orders). " +
238
+ "Batch is rejected before any API call if the sum exceeds this value. " +
239
+ "Market orders contribute 0 to the notional since their execution price is unknown.",
240
+ ),
191
241
  confirmation_token: z
192
242
  .string()
193
243
  .describe(
@@ -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
 
@@ -59,21 +61,20 @@ export async function handleCancelOrder(
59
61
 
60
62
  try {
61
63
  const data = await client.put<OrderResponse>(`/orders/${order_id}`, {
62
- state: "canceling",
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