@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.
- package/.cursor/rules/marketplace-docs-sync.mdc +32 -0
- package/CHANGELOG.md +75 -0
- package/PUBLISH_CHECKLIST.md +48 -89
- package/README.md +446 -78
- package/dist/audit.d.ts +21 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +14 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -1
- package/dist/http.js +65 -7
- package/dist/index.js +12 -3
- package/dist/tools/account.js +1 -1
- package/dist/tools/arbitrage.js +1 -1
- package/dist/tools/balance.js +1 -1
- package/dist/tools/balances.js +1 -1
- package/dist/tools/banks.js +1 -1
- package/dist/tools/batch_orders.d.ts +6 -1
- package/dist/tools/batch_orders.d.ts.map +1 -1
- package/dist/tools/batch_orders.js +47 -3
- package/dist/tools/cancel_all_orders.d.ts +1 -1
- package/dist/tools/cancel_all_orders.d.ts.map +1 -1
- package/dist/tools/cancel_all_orders.js +10 -13
- package/dist/tools/cancel_order.d.ts +1 -1
- package/dist/tools/cancel_order.d.ts.map +1 -1
- package/dist/tools/cancel_order.js +10 -10
- package/dist/tools/cancel_order_by_client_id.d.ts +1 -1
- package/dist/tools/cancel_order_by_client_id.d.ts.map +1 -1
- package/dist/tools/cancel_order_by_client_id.js +9 -9
- package/dist/tools/compare_markets.d.ts +9 -0
- package/dist/tools/compare_markets.d.ts.map +1 -1
- package/dist/tools/compare_markets.js +63 -53
- package/dist/tools/dead_mans_switch.d.ts +2 -2
- package/dist/tools/dead_mans_switch.d.ts.map +1 -1
- package/dist/tools/dead_mans_switch.js +68 -6
- package/dist/tools/deposits.js +2 -2
- package/dist/tools/fees.js +1 -1
- package/dist/tools/lightning.d.ts +1 -1
- package/dist/tools/lightning.d.ts.map +1 -1
- package/dist/tools/lightning.js +25 -9
- package/dist/tools/market_sentiment.js +1 -1
- package/dist/tools/market_summary.js +1 -1
- package/dist/tools/markets.js +1 -1
- package/dist/tools/order_lookup.js +2 -2
- package/dist/tools/orderbook.js +1 -1
- package/dist/tools/orders.js +1 -1
- package/dist/tools/place_order.d.ts +1 -1
- package/dist/tools/place_order.d.ts.map +1 -1
- package/dist/tools/place_order.js +53 -4
- package/dist/tools/price_history.js +1 -1
- package/dist/tools/quotation.js +1 -1
- package/dist/tools/receive_addresses.d.ts +6 -1
- package/dist/tools/receive_addresses.d.ts.map +1 -1
- package/dist/tools/receive_addresses.js +37 -13
- package/dist/tools/remittance_recipients.js +2 -2
- package/dist/tools/remittances.d.ts +7 -2
- package/dist/tools/remittances.d.ts.map +1 -1
- package/dist/tools/remittances.js +46 -23
- package/dist/tools/simulate_order.js +1 -1
- package/dist/tools/spread.js +1 -1
- package/dist/tools/technical_indicators.d.ts.map +1 -1
- package/dist/tools/technical_indicators.js +3 -2
- package/dist/tools/ticker.js +1 -1
- package/dist/tools/trades.js +1 -1
- package/dist/tools/volume.js +1 -1
- package/dist/tools/withdrawals.d.ts +1 -1
- package/dist/tools/withdrawals.d.ts.map +1 -1
- package/dist/tools/withdrawals.js +21 -11
- package/dist/utils.d.ts +10 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +29 -1
- package/dist/validation.d.ts +6 -0
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +26 -0
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +8 -1
- package/marketplace/README.md +1 -1
- package/marketplace/claude-listing.md +75 -4
- package/marketplace/gemini-tools.json +325 -2
- package/marketplace/openapi.yaml +160 -1
- package/package.json +2 -1
- package/server.json +2 -2
- package/src/audit.ts +24 -0
- package/src/client.ts +3 -1
- package/src/http.ts +75 -7
- package/src/index.ts +10 -3
- package/src/tools/account.ts +1 -1
- package/src/tools/arbitrage.ts +1 -1
- package/src/tools/balance.ts +1 -1
- package/src/tools/balances.ts +1 -1
- package/src/tools/banks.ts +1 -1
- package/src/tools/batch_orders.ts +52 -2
- package/src/tools/cancel_all_orders.ts +10 -12
- package/src/tools/cancel_order.ts +10 -9
- package/src/tools/cancel_order_by_client_id.ts +9 -8
- package/src/tools/compare_markets.ts +78 -61
- package/src/tools/dead_mans_switch.ts +76 -5
- package/src/tools/deposits.ts +2 -2
- package/src/tools/fees.ts +1 -1
- package/src/tools/lightning.ts +28 -9
- package/src/tools/market_sentiment.ts +1 -1
- package/src/tools/market_summary.ts +1 -1
- package/src/tools/markets.ts +1 -1
- package/src/tools/order_lookup.ts +2 -2
- package/src/tools/orderbook.ts +1 -1
- package/src/tools/orders.ts +1 -1
- package/src/tools/place_order.ts +56 -5
- package/src/tools/price_history.ts +1 -1
- package/src/tools/quotation.ts +1 -1
- package/src/tools/receive_addresses.ts +40 -13
- package/src/tools/remittance_recipients.ts +2 -2
- package/src/tools/remittances.ts +49 -22
- package/src/tools/simulate_order.ts +1 -1
- package/src/tools/spread.ts +1 -1
- package/src/tools/technical_indicators.ts +3 -2
- package/src/tools/ticker.ts +1 -1
- package/src/tools/trades.ts +1 -1
- package/src/tools/volume.ts +1 -1
- package/src/tools/withdrawals.ts +22 -10
- package/src/utils.ts +36 -1
- package/src/validation.ts +29 -0
- package/src/version.ts +11 -3
- 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.
|
|
9
|
+
"version": "1.5.2",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@guiie/buda-mcp",
|
|
14
|
-
"version": "1.5.
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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].
|
|
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
|
|
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
|
|
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].
|
|
153
|
+
const text = result.content[0]?.text ?? JSON.stringify({ error: "No content returned" });
|
|
147
154
|
return {
|
|
148
155
|
contents: [
|
|
149
156
|
{
|
package/src/tools/account.ts
CHANGED
|
@@ -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
|
|
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) }],
|
package/src/tools/arbitrage.ts
CHANGED
|
@@ -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
|
|
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) }],
|
package/src/tools/balance.ts
CHANGED
|
@@ -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
|
|
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) }],
|
package/src/tools/balances.ts
CHANGED
|
@@ -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
|
|
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) }],
|
package/src/tools/banks.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
|
89
|
+
? { error: err.message, code: err.status }
|
|
91
90
|
: { error: String(err), code: "UNKNOWN" };
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
|
73
|
+
? { error: err.message, code: err.status }
|
|
72
74
|
: { error: String(err), code: "UNKNOWN" };
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
|
107
|
+
? { error: err.message, code: err.status }
|
|
106
108
|
: { error: String(err), code: "UNKNOWN" };
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|