@guiie/buda-mcp 1.4.2 → 1.5.0
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/CHANGELOG.md +39 -0
- package/dist/cache.d.ts +1 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +1 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +16 -0
- package/dist/http.js +55 -0
- package/dist/index.js +30 -0
- package/dist/tools/account.d.ts +19 -0
- package/dist/tools/account.d.ts.map +1 -0
- package/dist/tools/account.js +49 -0
- package/dist/tools/balance.d.ts +29 -0
- package/dist/tools/balance.d.ts.map +1 -0
- package/dist/tools/balance.js +72 -0
- package/dist/tools/banks.d.ts +28 -0
- package/dist/tools/banks.d.ts.map +1 -0
- package/dist/tools/banks.js +68 -0
- package/dist/tools/batch_orders.d.ts +77 -0
- package/dist/tools/batch_orders.d.ts.map +1 -0
- package/dist/tools/batch_orders.js +154 -0
- package/dist/tools/cancel_all_orders.d.ts +34 -0
- package/dist/tools/cancel_all_orders.d.ts.map +1 -0
- package/dist/tools/cancel_all_orders.js +89 -0
- package/dist/tools/cancel_order_by_client_id.d.ts +34 -0
- package/dist/tools/cancel_order_by_client_id.d.ts.map +1 -0
- package/dist/tools/cancel_order_by_client_id.js +102 -0
- package/dist/tools/deposits.d.ts +83 -0
- package/dist/tools/deposits.d.ts.map +1 -0
- package/dist/tools/deposits.js +174 -0
- package/dist/tools/fees.d.ts +34 -0
- package/dist/tools/fees.d.ts.map +1 -0
- package/dist/tools/fees.js +72 -0
- package/dist/tools/lightning.d.ts +68 -0
- package/dist/tools/lightning.d.ts.map +1 -0
- package/dist/tools/lightning.js +171 -0
- package/dist/tools/order_lookup.d.ts +50 -0
- package/dist/tools/order_lookup.d.ts.map +1 -0
- package/dist/tools/order_lookup.js +112 -0
- package/dist/tools/place_order.d.ts +30 -0
- package/dist/tools/place_order.d.ts.map +1 -1
- package/dist/tools/place_order.js +100 -2
- package/dist/tools/quotation.d.ts +44 -0
- package/dist/tools/quotation.d.ts.map +1 -0
- package/dist/tools/quotation.js +99 -0
- package/dist/tools/receive_addresses.d.ts +78 -0
- package/dist/tools/receive_addresses.d.ts.map +1 -0
- package/dist/tools/receive_addresses.js +161 -0
- package/dist/tools/remittance_recipients.d.ts +54 -0
- package/dist/tools/remittance_recipients.d.ts.map +1 -0
- package/dist/tools/remittance_recipients.js +106 -0
- package/dist/tools/remittances.d.ts +115 -0
- package/dist/tools/remittances.d.ts.map +1 -0
- package/dist/tools/remittances.js +237 -0
- package/dist/tools/simulate_order.d.ts.map +1 -1
- package/dist/tools/simulate_order.js +2 -1
- package/dist/tools/withdrawals.d.ts +93 -0
- package/dist/tools/withdrawals.d.ts.map +1 -0
- package/dist/tools/withdrawals.js +215 -0
- package/dist/types.d.ts +155 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts +5 -0
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +12 -0
- package/marketplace/README.md +1 -1
- package/marketplace/claude-listing.md +30 -2
- package/marketplace/gemini-tools.json +155 -1
- package/marketplace/openapi.yaml +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/cache.ts +1 -0
- package/src/client.ts +20 -0
- package/src/http.ts +55 -0
- package/src/index.ts +30 -0
- package/src/tools/account.ts +66 -0
- package/src/tools/balance.ts +94 -0
- package/src/tools/banks.ts +94 -0
- package/src/tools/batch_orders.ts +199 -0
- package/src/tools/cancel_all_orders.ts +117 -0
- package/src/tools/cancel_order_by_client_id.ts +132 -0
- package/src/tools/deposits.ts +230 -0
- package/src/tools/fees.ts +91 -0
- package/src/tools/lightning.ts +231 -0
- package/src/tools/order_lookup.ts +139 -0
- package/src/tools/place_order.ts +119 -2
- package/src/tools/quotation.ts +124 -0
- package/src/tools/receive_addresses.ts +216 -0
- package/src/tools/remittance_recipients.ts +139 -0
- package/src/tools/remittances.ts +299 -0
- package/src/tools/simulate_order.ts +1 -0
- package/src/tools/withdrawals.ts +276 -0
- package/src/types.ts +210 -0
- package/src/validation.ts +16 -0
- package/test/run-all.ts +16 -0
- package/test/unit.ts +1905 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { BudaClient, BudaApiError } from "../client.js";
|
|
4
|
+
import { flattenAmount } from "../utils.js";
|
|
5
|
+
import type { LightningWithdrawalResponse, LightningInvoiceResponse } from "../types.js";
|
|
6
|
+
|
|
7
|
+
export const lightningWithdrawalToolSchema = {
|
|
8
|
+
name: "lightning_withdrawal",
|
|
9
|
+
description:
|
|
10
|
+
"Pay a Bitcoin Lightning Network invoice from your Buda.com LN-BTC reserve. " +
|
|
11
|
+
"IMPORTANT: Funds leave the account immediately on success. " +
|
|
12
|
+
"Pass confirmation_token='CONFIRM' to execute. " +
|
|
13
|
+
"Requires BUDA_API_KEY and BUDA_API_SECRET.",
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: "object" as const,
|
|
16
|
+
properties: {
|
|
17
|
+
invoice: {
|
|
18
|
+
type: "string",
|
|
19
|
+
description: "BOLT-11 Lightning invoice string (starts with 'lnbc', 'lntb', etc.).",
|
|
20
|
+
},
|
|
21
|
+
confirmation_token: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description:
|
|
24
|
+
"Safety confirmation. Must equal exactly 'CONFIRM' (case-sensitive) to execute. " +
|
|
25
|
+
"Any other value will reject the request without paying.",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
required: ["invoice", "confirmation_token"],
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const createLightningInvoiceToolSchema = {
|
|
33
|
+
name: "create_lightning_invoice",
|
|
34
|
+
description:
|
|
35
|
+
"Create a Bitcoin Lightning Network invoice on Buda.com to receive a payment. " +
|
|
36
|
+
"No funds leave the account — no confirmation required. " +
|
|
37
|
+
"Requires BUDA_API_KEY and BUDA_API_SECRET.",
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: "object" as const,
|
|
40
|
+
properties: {
|
|
41
|
+
amount_satoshis: {
|
|
42
|
+
type: "number",
|
|
43
|
+
description: "Invoice amount in satoshis (positive integer).",
|
|
44
|
+
},
|
|
45
|
+
description: {
|
|
46
|
+
type: "string",
|
|
47
|
+
description: "Optional payment description (max 140 characters).",
|
|
48
|
+
},
|
|
49
|
+
expiry_seconds: {
|
|
50
|
+
type: "number",
|
|
51
|
+
description: "Invoice expiry in seconds (60–86400, default: 3600).",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
required: ["amount_satoshis"],
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type LightningWithdrawalArgs = {
|
|
59
|
+
invoice: string;
|
|
60
|
+
confirmation_token: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type CreateLightningInvoiceArgs = {
|
|
64
|
+
amount_satoshis: number;
|
|
65
|
+
description?: string;
|
|
66
|
+
expiry_seconds?: number;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export async function handleLightningWithdrawal(
|
|
70
|
+
args: LightningWithdrawalArgs,
|
|
71
|
+
client: BudaClient,
|
|
72
|
+
): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
|
|
73
|
+
const { invoice, confirmation_token } = args;
|
|
74
|
+
|
|
75
|
+
if (confirmation_token !== "CONFIRM") {
|
|
76
|
+
const preview = invoice.length > 20 ? invoice.substring(0, 20) + "..." : invoice;
|
|
77
|
+
return {
|
|
78
|
+
content: [
|
|
79
|
+
{
|
|
80
|
+
type: "text",
|
|
81
|
+
text: JSON.stringify({
|
|
82
|
+
error:
|
|
83
|
+
"Lightning withdrawal not executed. confirmation_token must equal 'CONFIRM' to execute. " +
|
|
84
|
+
"Review the invoice and set confirmation_token='CONFIRM' to proceed.",
|
|
85
|
+
code: "CONFIRMATION_REQUIRED",
|
|
86
|
+
preview: { invoice_preview: preview },
|
|
87
|
+
}),
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
isError: true,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const data = await client.post<LightningWithdrawalResponse>(
|
|
96
|
+
`/reserves/ln-btc/withdrawals`,
|
|
97
|
+
{ invoice },
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const lw = data.lightning_withdrawal;
|
|
101
|
+
const amount = flattenAmount(lw.amount);
|
|
102
|
+
const fee = flattenAmount(lw.fee);
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
content: [
|
|
106
|
+
{
|
|
107
|
+
type: "text",
|
|
108
|
+
text: JSON.stringify(
|
|
109
|
+
{
|
|
110
|
+
id: lw.id,
|
|
111
|
+
state: lw.state,
|
|
112
|
+
amount: amount.value,
|
|
113
|
+
amount_currency: amount.currency,
|
|
114
|
+
fee: fee.value,
|
|
115
|
+
fee_currency: fee.currency,
|
|
116
|
+
payment_hash: lw.payment_hash,
|
|
117
|
+
created_at: lw.created_at,
|
|
118
|
+
},
|
|
119
|
+
null,
|
|
120
|
+
2,
|
|
121
|
+
),
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
};
|
|
125
|
+
} catch (err) {
|
|
126
|
+
const msg =
|
|
127
|
+
err instanceof BudaApiError
|
|
128
|
+
? { error: err.message, code: err.status, path: err.path }
|
|
129
|
+
: { error: String(err), code: "UNKNOWN" };
|
|
130
|
+
return {
|
|
131
|
+
content: [{ type: "text", text: JSON.stringify(msg) }],
|
|
132
|
+
isError: true,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function handleCreateLightningInvoice(
|
|
138
|
+
args: CreateLightningInvoiceArgs,
|
|
139
|
+
client: BudaClient,
|
|
140
|
+
): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
|
|
141
|
+
const { amount_satoshis, description, expiry_seconds } = args;
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const invoicePayload: Record<string, unknown> = { amount: amount_satoshis };
|
|
145
|
+
if (description !== undefined) invoicePayload.description = description;
|
|
146
|
+
if (expiry_seconds !== undefined) invoicePayload.expiry = expiry_seconds;
|
|
147
|
+
|
|
148
|
+
const data = await client.post<LightningInvoiceResponse>(
|
|
149
|
+
`/lightning_network_invoices`,
|
|
150
|
+
{ lightning_network_invoice: invoicePayload },
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const inv = data.lightning_network_invoice;
|
|
154
|
+
const amount = flattenAmount(inv.amount);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
content: [
|
|
158
|
+
{
|
|
159
|
+
type: "text",
|
|
160
|
+
text: JSON.stringify(
|
|
161
|
+
{
|
|
162
|
+
id: inv.id,
|
|
163
|
+
payment_request: inv.payment_request,
|
|
164
|
+
amount_satoshis: amount.value,
|
|
165
|
+
description: inv.description,
|
|
166
|
+
expires_at: inv.expires_at,
|
|
167
|
+
state: inv.state,
|
|
168
|
+
created_at: inv.created_at,
|
|
169
|
+
},
|
|
170
|
+
null,
|
|
171
|
+
2,
|
|
172
|
+
),
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
};
|
|
176
|
+
} catch (err) {
|
|
177
|
+
const msg =
|
|
178
|
+
err instanceof BudaApiError
|
|
179
|
+
? { error: err.message, code: err.status, path: err.path }
|
|
180
|
+
: { error: String(err), code: "UNKNOWN" };
|
|
181
|
+
return {
|
|
182
|
+
content: [{ type: "text", text: JSON.stringify(msg) }],
|
|
183
|
+
isError: true,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function register(server: McpServer, client: BudaClient): void {
|
|
189
|
+
server.tool(
|
|
190
|
+
lightningWithdrawalToolSchema.name,
|
|
191
|
+
lightningWithdrawalToolSchema.description,
|
|
192
|
+
{
|
|
193
|
+
invoice: z
|
|
194
|
+
.string()
|
|
195
|
+
.min(1)
|
|
196
|
+
.describe("BOLT-11 Lightning invoice string (starts with 'lnbc', 'lntb', etc.)."),
|
|
197
|
+
confirmation_token: z
|
|
198
|
+
.string()
|
|
199
|
+
.describe(
|
|
200
|
+
"Safety confirmation. Must equal exactly 'CONFIRM' (case-sensitive) to execute. " +
|
|
201
|
+
"Any other value will reject the request without paying.",
|
|
202
|
+
),
|
|
203
|
+
},
|
|
204
|
+
(args) => handleLightningWithdrawal(args, client),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
server.tool(
|
|
208
|
+
createLightningInvoiceToolSchema.name,
|
|
209
|
+
createLightningInvoiceToolSchema.description,
|
|
210
|
+
{
|
|
211
|
+
amount_satoshis: z
|
|
212
|
+
.number()
|
|
213
|
+
.int()
|
|
214
|
+
.positive()
|
|
215
|
+
.describe("Invoice amount in satoshis (positive integer)."),
|
|
216
|
+
description: z
|
|
217
|
+
.string()
|
|
218
|
+
.max(140)
|
|
219
|
+
.optional()
|
|
220
|
+
.describe("Optional payment description (max 140 characters)."),
|
|
221
|
+
expiry_seconds: z
|
|
222
|
+
.number()
|
|
223
|
+
.int()
|
|
224
|
+
.min(60)
|
|
225
|
+
.max(86400)
|
|
226
|
+
.optional()
|
|
227
|
+
.describe("Invoice expiry in seconds (60–86400, default: 3600)."),
|
|
228
|
+
},
|
|
229
|
+
(args) => handleCreateLightningInvoice(args, client),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { BudaClient, BudaApiError } from "../client.js";
|
|
4
|
+
import { flattenAmount } from "../utils.js";
|
|
5
|
+
import type { OrderResponse, Order } from "../types.js";
|
|
6
|
+
|
|
7
|
+
export const getOrderToolSchema = {
|
|
8
|
+
name: "get_order",
|
|
9
|
+
description:
|
|
10
|
+
"Returns a single order by its numeric ID on Buda.com. " +
|
|
11
|
+
"Fetches full detail including state, amounts, fees, and timestamps. " +
|
|
12
|
+
"All monetary amounts are floats with separate _currency fields. " +
|
|
13
|
+
"Requires BUDA_API_KEY and BUDA_API_SECRET. " +
|
|
14
|
+
"Example: 'Show me the details of order 987654.'",
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: "object" as const,
|
|
17
|
+
properties: {
|
|
18
|
+
order_id: {
|
|
19
|
+
type: "number",
|
|
20
|
+
description: "The numeric ID of the order.",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
required: ["order_id"],
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const getOrderByClientIdToolSchema = {
|
|
28
|
+
name: "get_order_by_client_id",
|
|
29
|
+
description:
|
|
30
|
+
"Returns an order by the client-assigned ID you set at placement on Buda.com. " +
|
|
31
|
+
"All monetary amounts are floats with separate _currency fields. " +
|
|
32
|
+
"Requires BUDA_API_KEY and BUDA_API_SECRET. " +
|
|
33
|
+
"Example: 'Find my order with client ID my-bot-order-42.'",
|
|
34
|
+
inputSchema: {
|
|
35
|
+
type: "object" as const,
|
|
36
|
+
properties: {
|
|
37
|
+
client_id: {
|
|
38
|
+
type: "string",
|
|
39
|
+
description: "The client ID string assigned when placing the order.",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
required: ["client_id"],
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function normalizeOrder(o: Order) {
|
|
47
|
+
const amount = flattenAmount(o.amount);
|
|
48
|
+
const originalAmount = flattenAmount(o.original_amount);
|
|
49
|
+
const tradedAmount = flattenAmount(o.traded_amount);
|
|
50
|
+
const totalExchanged = flattenAmount(o.total_exchanged);
|
|
51
|
+
const paidFee = flattenAmount(o.paid_fee);
|
|
52
|
+
const limitPrice = o.limit ? flattenAmount(o.limit) : null;
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
id: o.id,
|
|
56
|
+
type: o.type,
|
|
57
|
+
state: o.state,
|
|
58
|
+
created_at: o.created_at,
|
|
59
|
+
market_id: o.market_id,
|
|
60
|
+
fee_currency: o.fee_currency,
|
|
61
|
+
price_type: o.price_type,
|
|
62
|
+
order_type: o.order_type,
|
|
63
|
+
client_id: o.client_id,
|
|
64
|
+
limit_price: limitPrice ? limitPrice.value : null,
|
|
65
|
+
limit_price_currency: limitPrice ? limitPrice.currency : null,
|
|
66
|
+
amount: amount.value,
|
|
67
|
+
amount_currency: amount.currency,
|
|
68
|
+
original_amount: originalAmount.value,
|
|
69
|
+
original_amount_currency: originalAmount.currency,
|
|
70
|
+
traded_amount: tradedAmount.value,
|
|
71
|
+
traded_amount_currency: tradedAmount.currency,
|
|
72
|
+
total_exchanged: totalExchanged.value,
|
|
73
|
+
total_exchanged_currency: totalExchanged.currency,
|
|
74
|
+
paid_fee: paidFee.value,
|
|
75
|
+
paid_fee_currency: paidFee.currency,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function handleGetOrder(
|
|
80
|
+
args: { order_id: number },
|
|
81
|
+
client: BudaClient,
|
|
82
|
+
): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
|
|
83
|
+
try {
|
|
84
|
+
const data = await client.get<OrderResponse>(`/orders/${args.order_id}`);
|
|
85
|
+
return {
|
|
86
|
+
content: [{ type: "text", text: JSON.stringify(normalizeOrder(data.order), 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
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function handleGetOrderByClientId(
|
|
101
|
+
args: { client_id: string },
|
|
102
|
+
client: BudaClient,
|
|
103
|
+
): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
|
|
104
|
+
try {
|
|
105
|
+
const data = await client.get<OrderResponse>(`/orders/by-client-id/${encodeURIComponent(args.client_id)}`);
|
|
106
|
+
return {
|
|
107
|
+
content: [{ type: "text", text: JSON.stringify(normalizeOrder(data.order), null, 2) }],
|
|
108
|
+
};
|
|
109
|
+
} catch (err) {
|
|
110
|
+
const msg =
|
|
111
|
+
err instanceof BudaApiError
|
|
112
|
+
? { error: err.message, code: err.status, path: err.path }
|
|
113
|
+
: { error: String(err), code: "UNKNOWN" };
|
|
114
|
+
return {
|
|
115
|
+
content: [{ type: "text", text: JSON.stringify(msg) }],
|
|
116
|
+
isError: true,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function register(server: McpServer, client: BudaClient): void {
|
|
122
|
+
server.tool(
|
|
123
|
+
getOrderToolSchema.name,
|
|
124
|
+
getOrderToolSchema.description,
|
|
125
|
+
{
|
|
126
|
+
order_id: z.number().int().positive().describe("The numeric ID of the order."),
|
|
127
|
+
},
|
|
128
|
+
(args) => handleGetOrder(args, client),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
server.tool(
|
|
132
|
+
getOrderByClientIdToolSchema.name,
|
|
133
|
+
getOrderByClientIdToolSchema.description,
|
|
134
|
+
{
|
|
135
|
+
client_id: z.string().min(1).describe("The client ID string assigned when placing the order."),
|
|
136
|
+
},
|
|
137
|
+
(args) => handleGetOrderByClientId(args, client),
|
|
138
|
+
);
|
|
139
|
+
}
|
package/src/tools/place_order.ts
CHANGED
|
@@ -8,6 +8,7 @@ export const toolSchema = {
|
|
|
8
8
|
name: "place_order",
|
|
9
9
|
description:
|
|
10
10
|
"Place a limit or market order on Buda.com. " +
|
|
11
|
+
"Supports optional time-in-force flags (ioc, fok, post_only, gtd_timestamp) and stop orders. " +
|
|
11
12
|
"IMPORTANT: To prevent accidental execution from ambiguous prompts, you must pass " +
|
|
12
13
|
"confirmation_token='CONFIRM' to execute the order. " +
|
|
13
14
|
"Requires BUDA_API_KEY and BUDA_API_SECRET environment variables. " +
|
|
@@ -37,6 +38,30 @@ export const toolSchema = {
|
|
|
37
38
|
"Limit price in quote currency. Required when price_type is 'limit'. " +
|
|
38
39
|
"For Bid orders: highest price you will pay. For Ask orders: lowest price you will accept.",
|
|
39
40
|
},
|
|
41
|
+
ioc: {
|
|
42
|
+
type: "boolean",
|
|
43
|
+
description: "Immediate-or-cancel: fill as much as possible, cancel the rest. Mutually exclusive with fok, post_only, gtd_timestamp.",
|
|
44
|
+
},
|
|
45
|
+
fok: {
|
|
46
|
+
type: "boolean",
|
|
47
|
+
description: "Fill-or-kill: fill the entire order or cancel it entirely. Mutually exclusive with ioc, post_only, gtd_timestamp.",
|
|
48
|
+
},
|
|
49
|
+
post_only: {
|
|
50
|
+
type: "boolean",
|
|
51
|
+
description: "Post-only: rejected if it would execute immediately as a taker. Mutually exclusive with ioc, fok, gtd_timestamp.",
|
|
52
|
+
},
|
|
53
|
+
gtd_timestamp: {
|
|
54
|
+
type: "string",
|
|
55
|
+
description: "Good-till-date: ISO 8601 datetime after which the order is canceled. Mutually exclusive with ioc, fok, post_only.",
|
|
56
|
+
},
|
|
57
|
+
stop_price: {
|
|
58
|
+
type: "number",
|
|
59
|
+
description: "Stop trigger price. Must be paired with stop_type.",
|
|
60
|
+
},
|
|
61
|
+
stop_type: {
|
|
62
|
+
type: "string",
|
|
63
|
+
description: "Stop trigger direction: '>=' triggers when price rises to stop_price, '<=' when it falls. Must be paired with stop_price.",
|
|
64
|
+
},
|
|
40
65
|
confirmation_token: {
|
|
41
66
|
type: "string",
|
|
42
67
|
description:
|
|
@@ -54,6 +79,12 @@ type PlaceOrderArgs = {
|
|
|
54
79
|
price_type: "limit" | "market";
|
|
55
80
|
amount: number;
|
|
56
81
|
limit_price?: number;
|
|
82
|
+
ioc?: boolean;
|
|
83
|
+
fok?: boolean;
|
|
84
|
+
post_only?: boolean;
|
|
85
|
+
gtd_timestamp?: string;
|
|
86
|
+
stop_price?: number;
|
|
87
|
+
stop_type?: ">=" | "<=";
|
|
57
88
|
confirmation_token: string;
|
|
58
89
|
};
|
|
59
90
|
|
|
@@ -61,7 +92,20 @@ export async function handlePlaceOrder(
|
|
|
61
92
|
args: PlaceOrderArgs,
|
|
62
93
|
client: BudaClient,
|
|
63
94
|
): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
|
|
64
|
-
const {
|
|
95
|
+
const {
|
|
96
|
+
market_id,
|
|
97
|
+
type,
|
|
98
|
+
price_type,
|
|
99
|
+
amount,
|
|
100
|
+
limit_price,
|
|
101
|
+
ioc,
|
|
102
|
+
fok,
|
|
103
|
+
post_only,
|
|
104
|
+
gtd_timestamp,
|
|
105
|
+
stop_price,
|
|
106
|
+
stop_type,
|
|
107
|
+
confirmation_token,
|
|
108
|
+
} = args;
|
|
65
109
|
|
|
66
110
|
if (confirmation_token !== "CONFIRM") {
|
|
67
111
|
return {
|
|
@@ -89,6 +133,41 @@ export async function handlePlaceOrder(
|
|
|
89
133
|
};
|
|
90
134
|
}
|
|
91
135
|
|
|
136
|
+
// Validate TIF mutual exclusivity
|
|
137
|
+
const tifFlags = [ioc, fok, post_only, gtd_timestamp !== undefined].filter(Boolean);
|
|
138
|
+
if (tifFlags.length > 1) {
|
|
139
|
+
return {
|
|
140
|
+
content: [
|
|
141
|
+
{
|
|
142
|
+
type: "text",
|
|
143
|
+
text: JSON.stringify({
|
|
144
|
+
error: "ioc, fok, post_only, and gtd_timestamp are mutually exclusive. Specify at most one.",
|
|
145
|
+
code: "VALIDATION_ERROR",
|
|
146
|
+
}),
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
isError: true,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Validate stop_price / stop_type must both be present or both absent
|
|
154
|
+
const hasStopPrice = stop_price !== undefined;
|
|
155
|
+
const hasStopType = stop_type !== undefined;
|
|
156
|
+
if (hasStopPrice !== hasStopType) {
|
|
157
|
+
return {
|
|
158
|
+
content: [
|
|
159
|
+
{
|
|
160
|
+
type: "text",
|
|
161
|
+
text: JSON.stringify({
|
|
162
|
+
error: "stop_price and stop_type must both be provided together.",
|
|
163
|
+
code: "VALIDATION_ERROR",
|
|
164
|
+
}),
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
isError: true,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
92
171
|
try {
|
|
93
172
|
const payload: Record<string, unknown> = {
|
|
94
173
|
type,
|
|
@@ -111,7 +190,20 @@ export async function handlePlaceOrder(
|
|
|
111
190
|
isError: true,
|
|
112
191
|
};
|
|
113
192
|
}
|
|
114
|
-
|
|
193
|
+
|
|
194
|
+
let limitType = "gtc";
|
|
195
|
+
if (ioc) limitType = "ioc";
|
|
196
|
+
else if (fok) limitType = "fok";
|
|
197
|
+
else if (post_only) limitType = "post_only";
|
|
198
|
+
else if (gtd_timestamp !== undefined) limitType = "gtd";
|
|
199
|
+
|
|
200
|
+
const limitObj: Record<string, unknown> = { price: limit_price, type: limitType };
|
|
201
|
+
if (gtd_timestamp !== undefined) limitObj.expiration = gtd_timestamp;
|
|
202
|
+
payload.limit = limitObj;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (hasStopPrice && hasStopType) {
|
|
206
|
+
payload.stop = { price: stop_price, type: stop_type };
|
|
115
207
|
}
|
|
116
208
|
|
|
117
209
|
const data = await client.post<OrderResponse>(
|
|
@@ -160,6 +252,31 @@ export function register(server: McpServer, client: BudaClient): void {
|
|
|
160
252
|
"Limit price in quote currency. Required when price_type is 'limit'. " +
|
|
161
253
|
"For Bid orders: highest price you will pay. For Ask orders: lowest price you will accept.",
|
|
162
254
|
),
|
|
255
|
+
ioc: z
|
|
256
|
+
.boolean()
|
|
257
|
+
.optional()
|
|
258
|
+
.describe("Immediate-or-cancel: fill as much as possible, cancel the rest. Mutually exclusive with fok, post_only, gtd_timestamp."),
|
|
259
|
+
fok: z
|
|
260
|
+
.boolean()
|
|
261
|
+
.optional()
|
|
262
|
+
.describe("Fill-or-kill: fill the entire order or cancel it entirely. Mutually exclusive with ioc, post_only, gtd_timestamp."),
|
|
263
|
+
post_only: z
|
|
264
|
+
.boolean()
|
|
265
|
+
.optional()
|
|
266
|
+
.describe("Post-only: rejected if it would execute immediately as a taker. Mutually exclusive with ioc, fok, gtd_timestamp."),
|
|
267
|
+
gtd_timestamp: z
|
|
268
|
+
.string()
|
|
269
|
+
.optional()
|
|
270
|
+
.describe("Good-till-date: ISO 8601 datetime after which the order is canceled. Mutually exclusive with ioc, fok, post_only."),
|
|
271
|
+
stop_price: z
|
|
272
|
+
.number()
|
|
273
|
+
.positive()
|
|
274
|
+
.optional()
|
|
275
|
+
.describe("Stop trigger price. Must be paired with stop_type."),
|
|
276
|
+
stop_type: z
|
|
277
|
+
.enum([">=", "<="])
|
|
278
|
+
.optional()
|
|
279
|
+
.describe("Stop trigger direction: '>=' triggers when price rises to stop_price, '<=' when it falls. Must be paired with stop_price."),
|
|
163
280
|
confirmation_token: z
|
|
164
281
|
.string()
|
|
165
282
|
.describe(
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { BudaClient, BudaApiError } from "../client.js";
|
|
4
|
+
import { validateMarketId } from "../validation.js";
|
|
5
|
+
import { flattenAmount } from "../utils.js";
|
|
6
|
+
import type { QuotationResponse } from "../types.js";
|
|
7
|
+
|
|
8
|
+
export const toolSchema = {
|
|
9
|
+
name: "get_real_quotation",
|
|
10
|
+
description:
|
|
11
|
+
"Gets a server-side price quotation for a buy or sell on Buda.com. " +
|
|
12
|
+
"Calls the Buda quotation API to compute an accurate fill estimate including fees, " +
|
|
13
|
+
"based on live order book state. Prefer this over simulate_order for accurate fee-tier-aware quotes. " +
|
|
14
|
+
"This is a POST (not idempotent) but does not place an order. Public endpoint — no API key required. " +
|
|
15
|
+
"Parameters: market_id, type ('Bid'|'Ask'), amount, optional limit price. " +
|
|
16
|
+
"Example: 'Get an accurate quote to sell 0.05 BTC on BTC-CLP.'",
|
|
17
|
+
inputSchema: {
|
|
18
|
+
type: "object" as const,
|
|
19
|
+
properties: {
|
|
20
|
+
market_id: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "Market ID (e.g. 'BTC-CLP', 'ETH-BTC').",
|
|
23
|
+
},
|
|
24
|
+
type: {
|
|
25
|
+
type: "string",
|
|
26
|
+
description: "'Bid' to buy base currency, 'Ask' to sell base currency.",
|
|
27
|
+
},
|
|
28
|
+
amount: {
|
|
29
|
+
type: "number",
|
|
30
|
+
description: "Order size (positive number).",
|
|
31
|
+
},
|
|
32
|
+
limit: {
|
|
33
|
+
type: "number",
|
|
34
|
+
description: "Optional limit price in quote currency.",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ["market_id", "type", "amount"],
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type GetRealQuotationArgs = {
|
|
42
|
+
market_id: string;
|
|
43
|
+
type: "Bid" | "Ask";
|
|
44
|
+
amount: number;
|
|
45
|
+
limit?: number;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export async function handleGetRealQuotation(
|
|
49
|
+
args: GetRealQuotationArgs,
|
|
50
|
+
client: BudaClient,
|
|
51
|
+
): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
|
|
52
|
+
const { market_id, type, amount, limit } = args;
|
|
53
|
+
|
|
54
|
+
const validationError = validateMarketId(market_id);
|
|
55
|
+
if (validationError) {
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_MARKET_ID" }) }],
|
|
58
|
+
isError: true,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const id = market_id.toLowerCase();
|
|
63
|
+
const payload: Record<string, unknown> = { type, amount: String(amount) };
|
|
64
|
+
if (limit !== undefined) payload.limit = String(limit);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const data = await client.post<QuotationResponse>(`/markets/${id}/quotations`, {
|
|
68
|
+
quotation: payload,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const q = data.quotation;
|
|
72
|
+
const flatAmount = flattenAmount(q.amount);
|
|
73
|
+
const flatLimit = q.limit ? flattenAmount(q.limit) : null;
|
|
74
|
+
const flatBase = flattenAmount(q.base_balance_change);
|
|
75
|
+
const flatQuote = flattenAmount(q.quote_balance_change);
|
|
76
|
+
const flatFee = flattenAmount(q.fee_amount);
|
|
77
|
+
const flatOrder = flattenAmount(q.order_amount);
|
|
78
|
+
|
|
79
|
+
const result = {
|
|
80
|
+
id: q.id ?? null,
|
|
81
|
+
type: q.type,
|
|
82
|
+
market_id: q.market_id,
|
|
83
|
+
amount: flatAmount.value,
|
|
84
|
+
amount_currency: flatAmount.currency,
|
|
85
|
+
limit: flatLimit ? flatLimit.value : null,
|
|
86
|
+
limit_currency: flatLimit ? flatLimit.currency : null,
|
|
87
|
+
base_balance_change: flatBase.value,
|
|
88
|
+
base_balance_change_currency: flatBase.currency,
|
|
89
|
+
quote_balance_change: flatQuote.value,
|
|
90
|
+
quote_balance_change_currency: flatQuote.currency,
|
|
91
|
+
fee_amount: flatFee.value,
|
|
92
|
+
fee_currency: flatFee.currency,
|
|
93
|
+
order_amount: flatOrder.value,
|
|
94
|
+
order_amount_currency: flatOrder.currency,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
99
|
+
};
|
|
100
|
+
} catch (err) {
|
|
101
|
+
const msg =
|
|
102
|
+
err instanceof BudaApiError
|
|
103
|
+
? { error: err.message, code: err.status, path: err.path }
|
|
104
|
+
: { error: String(err), code: "UNKNOWN" };
|
|
105
|
+
return {
|
|
106
|
+
content: [{ type: "text", text: JSON.stringify(msg) }],
|
|
107
|
+
isError: true,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function register(server: McpServer, client: BudaClient): void {
|
|
113
|
+
server.tool(
|
|
114
|
+
toolSchema.name,
|
|
115
|
+
toolSchema.description,
|
|
116
|
+
{
|
|
117
|
+
market_id: z.string().describe("Market ID (e.g. 'BTC-CLP', 'ETH-BTC')."),
|
|
118
|
+
type: z.enum(["Bid", "Ask"]).describe("'Bid' to buy base currency, 'Ask' to sell base currency."),
|
|
119
|
+
amount: z.number().positive().describe("Order size (positive number)."),
|
|
120
|
+
limit: z.number().positive().optional().describe("Optional limit price in quote currency."),
|
|
121
|
+
},
|
|
122
|
+
(args) => handleGetRealQuotation(args, client),
|
|
123
|
+
);
|
|
124
|
+
}
|