@guiie/buda-mcp 1.4.2 → 1.5.1

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 (113) hide show
  1. package/.cursor/rules/marketplace-docs-sync.mdc +32 -0
  2. package/CHANGELOG.md +79 -0
  3. package/PUBLISH_CHECKLIST.md +40 -88
  4. package/README.md +446 -78
  5. package/dist/cache.d.ts +1 -0
  6. package/dist/cache.d.ts.map +1 -1
  7. package/dist/cache.js +1 -0
  8. package/dist/client.d.ts +2 -0
  9. package/dist/client.d.ts.map +1 -1
  10. package/dist/client.js +18 -1
  11. package/dist/http.js +97 -6
  12. package/dist/index.js +42 -3
  13. package/dist/tools/account.d.ts +19 -0
  14. package/dist/tools/account.d.ts.map +1 -0
  15. package/dist/tools/account.js +49 -0
  16. package/dist/tools/balance.d.ts +29 -0
  17. package/dist/tools/balance.d.ts.map +1 -0
  18. package/dist/tools/balance.js +72 -0
  19. package/dist/tools/banks.d.ts +28 -0
  20. package/dist/tools/banks.d.ts.map +1 -0
  21. package/dist/tools/banks.js +68 -0
  22. package/dist/tools/batch_orders.d.ts +82 -0
  23. package/dist/tools/batch_orders.d.ts.map +1 -0
  24. package/dist/tools/batch_orders.js +188 -0
  25. package/dist/tools/cancel_all_orders.d.ts +34 -0
  26. package/dist/tools/cancel_all_orders.d.ts.map +1 -0
  27. package/dist/tools/cancel_all_orders.js +89 -0
  28. package/dist/tools/cancel_order.js +1 -1
  29. package/dist/tools/cancel_order_by_client_id.d.ts +34 -0
  30. package/dist/tools/cancel_order_by_client_id.d.ts.map +1 -0
  31. package/dist/tools/cancel_order_by_client_id.js +102 -0
  32. package/dist/tools/dead_mans_switch.d.ts +1 -1
  33. package/dist/tools/dead_mans_switch.d.ts.map +1 -1
  34. package/dist/tools/dead_mans_switch.js +33 -3
  35. package/dist/tools/deposits.d.ts +83 -0
  36. package/dist/tools/deposits.d.ts.map +1 -0
  37. package/dist/tools/deposits.js +174 -0
  38. package/dist/tools/fees.d.ts +34 -0
  39. package/dist/tools/fees.d.ts.map +1 -0
  40. package/dist/tools/fees.js +72 -0
  41. package/dist/tools/lightning.d.ts +68 -0
  42. package/dist/tools/lightning.d.ts.map +1 -0
  43. package/dist/tools/lightning.js +185 -0
  44. package/dist/tools/order_lookup.d.ts +50 -0
  45. package/dist/tools/order_lookup.d.ts.map +1 -0
  46. package/dist/tools/order_lookup.js +112 -0
  47. package/dist/tools/place_order.d.ts +30 -0
  48. package/dist/tools/place_order.d.ts.map +1 -1
  49. package/dist/tools/place_order.js +131 -2
  50. package/dist/tools/quotation.d.ts +44 -0
  51. package/dist/tools/quotation.d.ts.map +1 -0
  52. package/dist/tools/quotation.js +99 -0
  53. package/dist/tools/receive_addresses.d.ts +83 -0
  54. package/dist/tools/receive_addresses.d.ts.map +1 -0
  55. package/dist/tools/receive_addresses.js +185 -0
  56. package/dist/tools/remittance_recipients.d.ts +54 -0
  57. package/dist/tools/remittance_recipients.d.ts.map +1 -0
  58. package/dist/tools/remittance_recipients.js +106 -0
  59. package/dist/tools/remittances.d.ts +120 -0
  60. package/dist/tools/remittances.d.ts.map +1 -0
  61. package/dist/tools/remittances.js +261 -0
  62. package/dist/tools/simulate_order.d.ts.map +1 -1
  63. package/dist/tools/simulate_order.js +2 -1
  64. package/dist/tools/technical_indicators.d.ts.map +1 -1
  65. package/dist/tools/technical_indicators.js +2 -1
  66. package/dist/tools/withdrawals.d.ts +93 -0
  67. package/dist/tools/withdrawals.d.ts.map +1 -0
  68. package/dist/tools/withdrawals.js +225 -0
  69. package/dist/types.d.ts +155 -0
  70. package/dist/types.d.ts.map +1 -1
  71. package/dist/utils.d.ts.map +1 -1
  72. package/dist/utils.js +4 -1
  73. package/dist/validation.d.ts +11 -0
  74. package/dist/validation.d.ts.map +1 -1
  75. package/dist/validation.js +38 -0
  76. package/dist/version.d.ts.map +1 -1
  77. package/dist/version.js +8 -1
  78. package/marketplace/README.md +1 -1
  79. package/marketplace/claude-listing.md +101 -2
  80. package/marketplace/gemini-tools.json +478 -1
  81. package/marketplace/openapi.yaml +160 -1
  82. package/package.json +2 -1
  83. package/server.json +2 -2
  84. package/src/cache.ts +1 -0
  85. package/src/client.ts +23 -1
  86. package/src/http.ts +105 -6
  87. package/src/index.ts +40 -3
  88. package/src/tools/account.ts +66 -0
  89. package/src/tools/balance.ts +94 -0
  90. package/src/tools/banks.ts +94 -0
  91. package/src/tools/batch_orders.ts +238 -0
  92. package/src/tools/cancel_all_orders.ts +117 -0
  93. package/src/tools/cancel_order.ts +1 -1
  94. package/src/tools/cancel_order_by_client_id.ts +132 -0
  95. package/src/tools/dead_mans_switch.ts +39 -3
  96. package/src/tools/deposits.ts +230 -0
  97. package/src/tools/fees.ts +91 -0
  98. package/src/tools/lightning.ts +247 -0
  99. package/src/tools/order_lookup.ts +139 -0
  100. package/src/tools/place_order.ts +151 -2
  101. package/src/tools/quotation.ts +124 -0
  102. package/src/tools/receive_addresses.ts +242 -0
  103. package/src/tools/remittance_recipients.ts +139 -0
  104. package/src/tools/remittances.ts +325 -0
  105. package/src/tools/simulate_order.ts +1 -0
  106. package/src/tools/technical_indicators.ts +2 -1
  107. package/src/tools/withdrawals.ts +287 -0
  108. package/src/types.ts +210 -0
  109. package/src/utils.ts +3 -1
  110. package/src/validation.ts +45 -0
  111. package/src/version.ts +11 -3
  112. package/test/run-all.ts +16 -0
  113. package/test/unit.ts +2149 -1
@@ -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 { market_id, type, price_type, amount, limit_price, confirmation_token } = args;
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,52 @@ export async function handlePlaceOrder(
111
190
  isError: true,
112
191
  };
113
192
  }
114
- payload.limit = { price: limit_price, type: "gtc" };
193
+
194
+ if (gtd_timestamp !== undefined) {
195
+ const ts = new Date(gtd_timestamp).getTime();
196
+ if (isNaN(ts)) {
197
+ return {
198
+ content: [
199
+ {
200
+ type: "text",
201
+ text: JSON.stringify({
202
+ error: "gtd_timestamp must be a valid ISO 8601 datetime string.",
203
+ code: "VALIDATION_ERROR",
204
+ }),
205
+ },
206
+ ],
207
+ isError: true,
208
+ };
209
+ }
210
+ if (ts <= Date.now()) {
211
+ return {
212
+ content: [
213
+ {
214
+ type: "text",
215
+ text: JSON.stringify({
216
+ error: "gtd_timestamp must be a future datetime.",
217
+ code: "VALIDATION_ERROR",
218
+ }),
219
+ },
220
+ ],
221
+ isError: true,
222
+ };
223
+ }
224
+ }
225
+
226
+ let limitType = "gtc";
227
+ if (ioc) limitType = "ioc";
228
+ else if (fok) limitType = "fok";
229
+ else if (post_only) limitType = "post_only";
230
+ else if (gtd_timestamp !== undefined) limitType = "gtd";
231
+
232
+ const limitObj: Record<string, unknown> = { price: limit_price, type: limitType };
233
+ if (gtd_timestamp !== undefined) limitObj.expiration = gtd_timestamp;
234
+ payload.limit = limitObj;
235
+ }
236
+
237
+ if (hasStopPrice && hasStopType) {
238
+ payload.stop = { price: stop_price, type: stop_type };
115
239
  }
116
240
 
117
241
  const data = await client.post<OrderResponse>(
@@ -160,6 +284,31 @@ export function register(server: McpServer, client: BudaClient): void {
160
284
  "Limit price in quote currency. Required when price_type is 'limit'. " +
161
285
  "For Bid orders: highest price you will pay. For Ask orders: lowest price you will accept.",
162
286
  ),
287
+ ioc: z
288
+ .boolean()
289
+ .optional()
290
+ .describe("Immediate-or-cancel: fill as much as possible, cancel the rest. Mutually exclusive with fok, post_only, gtd_timestamp."),
291
+ fok: z
292
+ .boolean()
293
+ .optional()
294
+ .describe("Fill-or-kill: fill the entire order or cancel it entirely. Mutually exclusive with ioc, post_only, gtd_timestamp."),
295
+ post_only: z
296
+ .boolean()
297
+ .optional()
298
+ .describe("Post-only: rejected if it would execute immediately as a taker. Mutually exclusive with ioc, fok, gtd_timestamp."),
299
+ gtd_timestamp: z
300
+ .string()
301
+ .optional()
302
+ .describe("Good-till-date: ISO 8601 datetime after which the order is canceled. Mutually exclusive with ioc, fok, post_only."),
303
+ stop_price: z
304
+ .number()
305
+ .positive()
306
+ .optional()
307
+ .describe("Stop trigger price. Must be paired with stop_type."),
308
+ stop_type: z
309
+ .enum([">=", "<="])
310
+ .optional()
311
+ .describe("Stop trigger direction: '>=' triggers when price rises to stop_price, '<=' when it falls. Must be paired with stop_price."),
163
312
  confirmation_token: z
164
313
  .string()
165
314
  .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
+ }
@@ -0,0 +1,242 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { BudaClient, BudaApiError } from "../client.js";
4
+ import { validateCurrency } from "../validation.js";
5
+ import type { ReceiveAddressesResponse, SingleReceiveAddressResponse, ReceiveAddress } from "../types.js";
6
+
7
+ export const createReceiveAddressToolSchema = {
8
+ name: "create_receive_address",
9
+ description:
10
+ "Generates a new receive address for a crypto currency. " +
11
+ "Creates a new blockchain deposit address for the given currency. " +
12
+ "Each call generates a distinct address. Not idempotent. " +
13
+ "IMPORTANT: Pass confirmation_token='CONFIRM' to execute. " +
14
+ "Only applicable to crypto currencies (BTC, ETH, etc.). " +
15
+ "Requires BUDA_API_KEY and BUDA_API_SECRET. " +
16
+ "Example: 'Give me a fresh Bitcoin deposit address.'",
17
+ inputSchema: {
18
+ type: "object" as const,
19
+ properties: {
20
+ currency: {
21
+ type: "string",
22
+ description: "Currency code (e.g. 'BTC', 'ETH').",
23
+ },
24
+ confirmation_token: {
25
+ type: "string",
26
+ description: "Safety confirmation. Must equal exactly 'CONFIRM' (case-sensitive) to generate a new address.",
27
+ },
28
+ },
29
+ required: ["currency", "confirmation_token"],
30
+ },
31
+ };
32
+
33
+ export const listReceiveAddressesToolSchema = {
34
+ name: "list_receive_addresses",
35
+ description:
36
+ "Lists all receive (deposit) addresses for a crypto currency on the authenticated Buda.com account. " +
37
+ "Returns an empty array if no addresses have been created yet. " +
38
+ "Requires BUDA_API_KEY and BUDA_API_SECRET. " +
39
+ "Example: 'What are my Bitcoin deposit addresses?'",
40
+ inputSchema: {
41
+ type: "object" as const,
42
+ properties: {
43
+ currency: {
44
+ type: "string",
45
+ description: "Currency code (e.g. 'BTC', 'ETH').",
46
+ },
47
+ },
48
+ required: ["currency"],
49
+ },
50
+ };
51
+
52
+ export const getReceiveAddressToolSchema = {
53
+ name: "get_receive_address",
54
+ description:
55
+ "Returns a single receive address by its ID for a given currency on Buda.com. " +
56
+ "Requires BUDA_API_KEY and BUDA_API_SECRET. " +
57
+ "Example: 'Get the details of my BTC receive address ID 42.'",
58
+ inputSchema: {
59
+ type: "object" as const,
60
+ properties: {
61
+ currency: {
62
+ type: "string",
63
+ description: "Currency code (e.g. 'BTC', 'ETH').",
64
+ },
65
+ id: {
66
+ type: "number",
67
+ description: "The numeric ID of the receive address.",
68
+ },
69
+ },
70
+ required: ["currency", "id"],
71
+ },
72
+ };
73
+
74
+ function normalizeAddress(a: ReceiveAddress) {
75
+ return {
76
+ id: a.id,
77
+ address: a.address,
78
+ currency: a.currency,
79
+ created_at: a.created_at,
80
+ label: a.label ?? null,
81
+ };
82
+ }
83
+
84
+ export async function handleListReceiveAddresses(
85
+ args: { currency: string },
86
+ client: BudaClient,
87
+ ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
88
+ const { currency } = args;
89
+
90
+ const validationError = validateCurrency(currency);
91
+ if (validationError) {
92
+ return {
93
+ content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_CURRENCY" }) }],
94
+ isError: true,
95
+ };
96
+ }
97
+
98
+ try {
99
+ const data = await client.get<ReceiveAddressesResponse>(
100
+ `/currencies/${currency.toUpperCase()}/receive_addresses`,
101
+ );
102
+ return {
103
+ content: [
104
+ {
105
+ type: "text",
106
+ text: JSON.stringify(
107
+ { receive_addresses: data.receive_addresses.map(normalizeAddress) },
108
+ null,
109
+ 2,
110
+ ),
111
+ },
112
+ ],
113
+ };
114
+ } catch (err) {
115
+ const msg =
116
+ err instanceof BudaApiError
117
+ ? { error: err.message, code: err.status, path: err.path }
118
+ : { error: String(err), code: "UNKNOWN" };
119
+ return {
120
+ content: [{ type: "text", text: JSON.stringify(msg) }],
121
+ isError: true,
122
+ };
123
+ }
124
+ }
125
+
126
+ export async function handleGetReceiveAddress(
127
+ args: { currency: string; id: number },
128
+ client: BudaClient,
129
+ ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
130
+ const { currency, id } = args;
131
+
132
+ const validationError = validateCurrency(currency);
133
+ if (validationError) {
134
+ return {
135
+ content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_CURRENCY" }) }],
136
+ isError: true,
137
+ };
138
+ }
139
+
140
+ try {
141
+ const data = await client.get<SingleReceiveAddressResponse>(
142
+ `/currencies/${currency.toUpperCase()}/receive_addresses/${id}`,
143
+ );
144
+ return {
145
+ content: [{ type: "text", text: JSON.stringify(normalizeAddress(data.receive_address), null, 2) }],
146
+ };
147
+ } catch (err) {
148
+ const msg =
149
+ err instanceof BudaApiError
150
+ ? { error: err.message, code: err.status, path: err.path }
151
+ : { error: String(err), code: "UNKNOWN" };
152
+ return {
153
+ content: [{ type: "text", text: JSON.stringify(msg) }],
154
+ isError: true,
155
+ };
156
+ }
157
+ }
158
+
159
+ export async function handleCreateReceiveAddress(
160
+ args: { currency: string; confirmation_token: string },
161
+ client: BudaClient,
162
+ ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
163
+ const { currency, confirmation_token } = args;
164
+
165
+ if (confirmation_token !== "CONFIRM") {
166
+ return {
167
+ content: [
168
+ {
169
+ type: "text",
170
+ text: JSON.stringify({
171
+ error:
172
+ "Address not generated. confirmation_token must equal 'CONFIRM' to execute. " +
173
+ "Each call creates a distinct address — review and set confirmation_token='CONFIRM' to proceed.",
174
+ code: "CONFIRMATION_REQUIRED",
175
+ preview: { currency },
176
+ }),
177
+ },
178
+ ],
179
+ isError: true,
180
+ };
181
+ }
182
+
183
+ const validationError = validateCurrency(currency);
184
+ if (validationError) {
185
+ return {
186
+ content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_CURRENCY" }) }],
187
+ isError: true,
188
+ };
189
+ }
190
+
191
+ try {
192
+ const data = await client.post<SingleReceiveAddressResponse>(
193
+ `/currencies/${currency.toUpperCase()}/receive_addresses`,
194
+ {},
195
+ );
196
+ return {
197
+ content: [{ type: "text", text: JSON.stringify(normalizeAddress(data.receive_address), null, 2) }],
198
+ };
199
+ } catch (err) {
200
+ const msg =
201
+ err instanceof BudaApiError
202
+ ? { error: err.message, code: err.status, path: err.path }
203
+ : { error: String(err), code: "UNKNOWN" };
204
+ return {
205
+ content: [{ type: "text", text: JSON.stringify(msg) }],
206
+ isError: true,
207
+ };
208
+ }
209
+ }
210
+
211
+ export function register(server: McpServer, client: BudaClient): void {
212
+ server.tool(
213
+ listReceiveAddressesToolSchema.name,
214
+ listReceiveAddressesToolSchema.description,
215
+ {
216
+ currency: z.string().min(2).max(10).describe("Currency code (e.g. 'BTC', 'ETH')."),
217
+ },
218
+ (args) => handleListReceiveAddresses(args, client),
219
+ );
220
+
221
+ server.tool(
222
+ getReceiveAddressToolSchema.name,
223
+ getReceiveAddressToolSchema.description,
224
+ {
225
+ currency: z.string().min(2).max(10).describe("Currency code (e.g. 'BTC', 'ETH')."),
226
+ id: z.number().int().positive().describe("The numeric ID of the receive address."),
227
+ },
228
+ (args) => handleGetReceiveAddress(args, client),
229
+ );
230
+
231
+ server.tool(
232
+ createReceiveAddressToolSchema.name,
233
+ createReceiveAddressToolSchema.description,
234
+ {
235
+ currency: z.string().min(2).max(10).describe("Currency code (e.g. 'BTC', 'ETH')."),
236
+ confirmation_token: z
237
+ .string()
238
+ .describe("Safety confirmation. Must equal exactly 'CONFIRM' (case-sensitive) to generate a new address."),
239
+ },
240
+ (args) => handleCreateReceiveAddress(args, client),
241
+ );
242
+ }
@@ -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 type { RemittanceRecipientsResponse, SingleRemittanceRecipientResponse, RemittanceRecipient } from "../types.js";
5
+
6
+ export const listToolSchema = {
7
+ name: "list_remittance_recipients",
8
+ description:
9
+ "Lists all saved remittance recipients (bank accounts) for the authenticated Buda.com account. " +
10
+ "Supports pagination. " +
11
+ "Requires BUDA_API_KEY and BUDA_API_SECRET. " +
12
+ "Example: 'Who are my saved remittance recipients?'",
13
+ inputSchema: {
14
+ type: "object" as const,
15
+ properties: {
16
+ per: {
17
+ type: "number",
18
+ description: "Results per page (default: 20, max: 300).",
19
+ },
20
+ page: {
21
+ type: "number",
22
+ description: "Page number (default: 1).",
23
+ },
24
+ },
25
+ },
26
+ };
27
+
28
+ export const getToolSchema = {
29
+ name: "get_remittance_recipient",
30
+ description:
31
+ "Returns a single saved remittance recipient by its ID on Buda.com. " +
32
+ "Fetches saved bank details for one recipient. " +
33
+ "Requires BUDA_API_KEY and BUDA_API_SECRET. " +
34
+ "Example: 'Show remittance recipient ID 5.'",
35
+ inputSchema: {
36
+ type: "object" as const,
37
+ properties: {
38
+ id: {
39
+ type: "number",
40
+ description: "The numeric ID of the remittance recipient.",
41
+ },
42
+ },
43
+ required: ["id"],
44
+ },
45
+ };
46
+
47
+ function normalizeRecipient(r: RemittanceRecipient) {
48
+ return {
49
+ id: r.id,
50
+ name: r.name,
51
+ bank: r.bank,
52
+ account_number: r.account_number,
53
+ currency: r.currency,
54
+ country: r.country ?? null,
55
+ };
56
+ }
57
+
58
+ export async function handleListRemittanceRecipients(
59
+ args: { per?: number; page?: number },
60
+ client: BudaClient,
61
+ ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
62
+ try {
63
+ const params: Record<string, string | number> = {};
64
+ if (args.per !== undefined) params.per = args.per;
65
+ if (args.page !== undefined) params.page = args.page;
66
+
67
+ const data = await client.get<RemittanceRecipientsResponse>(
68
+ "/remittance_recipients",
69
+ Object.keys(params).length > 0 ? params : undefined,
70
+ );
71
+
72
+ return {
73
+ content: [
74
+ {
75
+ type: "text",
76
+ text: JSON.stringify(
77
+ {
78
+ remittance_recipients: data.remittance_recipients.map(normalizeRecipient),
79
+ meta: data.meta,
80
+ },
81
+ null,
82
+ 2,
83
+ ),
84
+ },
85
+ ],
86
+ };
87
+ } catch (err) {
88
+ const msg =
89
+ err instanceof BudaApiError
90
+ ? { error: err.message, code: err.status, path: err.path }
91
+ : { error: String(err), code: "UNKNOWN" };
92
+ return {
93
+ content: [{ type: "text", text: JSON.stringify(msg) }],
94
+ isError: true,
95
+ };
96
+ }
97
+ }
98
+
99
+ export async function handleGetRemittanceRecipient(
100
+ args: { id: number },
101
+ client: BudaClient,
102
+ ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
103
+ try {
104
+ const data = await client.get<SingleRemittanceRecipientResponse>(`/remittance_recipients/${args.id}`);
105
+ return {
106
+ content: [{ type: "text", text: JSON.stringify(normalizeRecipient(data.remittance_recipient), null, 2) }],
107
+ };
108
+ } catch (err) {
109
+ const msg =
110
+ err instanceof BudaApiError
111
+ ? { error: err.message, code: err.status, path: err.path }
112
+ : { error: String(err), code: "UNKNOWN" };
113
+ return {
114
+ content: [{ type: "text", text: JSON.stringify(msg) }],
115
+ isError: true,
116
+ };
117
+ }
118
+ }
119
+
120
+ export function register(server: McpServer, client: BudaClient): void {
121
+ server.tool(
122
+ listToolSchema.name,
123
+ listToolSchema.description,
124
+ {
125
+ per: z.number().int().min(1).max(300).optional().describe("Results per page (default: 20, max: 300)."),
126
+ page: z.number().int().min(1).optional().describe("Page number (default: 1)."),
127
+ },
128
+ (args) => handleListRemittanceRecipients(args, client),
129
+ );
130
+
131
+ server.tool(
132
+ getToolSchema.name,
133
+ getToolSchema.description,
134
+ {
135
+ id: z.number().int().positive().describe("The numeric ID of the remittance recipient."),
136
+ },
137
+ (args) => handleGetRemittanceRecipient(args, client),
138
+ );
139
+ }