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