@guiie/buda-mcp 1.1.1 → 1.2.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/src/cache.ts CHANGED
@@ -5,15 +5,31 @@ interface CacheEntry<T> {
5
5
 
6
6
  export class MemoryCache {
7
7
  private store = new Map<string, CacheEntry<unknown>>();
8
+ private inflight = new Map<string, Promise<unknown>>();
8
9
 
9
10
  async getOrFetch<T>(key: string, ttlMs: number, fetcher: () => Promise<T>): Promise<T> {
10
11
  const entry = this.store.get(key) as CacheEntry<T> | undefined;
11
12
  if (entry && Date.now() < entry.expiry) {
12
13
  return entry.data;
13
14
  }
14
- const data = await fetcher();
15
- this.store.set(key, { data, expiry: Date.now() + ttlMs });
16
- return data;
15
+
16
+ // Deduplicate concurrent requests for the same expired/missing key.
17
+ const pending = this.inflight.get(key) as Promise<T> | undefined;
18
+ if (pending) return pending;
19
+
20
+ const promise = fetcher()
21
+ .then((data) => {
22
+ this.store.set(key, { data, expiry: Date.now() + ttlMs });
23
+ this.inflight.delete(key);
24
+ return data;
25
+ })
26
+ .catch((err: unknown) => {
27
+ this.inflight.delete(key);
28
+ throw err;
29
+ });
30
+
31
+ this.inflight.set(key, promise as Promise<unknown>);
32
+ return promise;
17
33
  }
18
34
 
19
35
  invalidate(key: string): void {
package/src/client.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createHmac } from "crypto";
2
+ import { VERSION } from "./version.js";
2
3
 
3
4
  const BASE_URL = "https://www.buda.com/api/v2";
4
5
 
@@ -7,6 +8,7 @@ export class BudaApiError extends Error {
7
8
  public readonly status: number,
8
9
  public readonly path: string,
9
10
  message: string,
11
+ public readonly retryAfterMs?: number,
10
12
  ) {
11
13
  super(message);
12
14
  this.name = "BudaApiError";
@@ -56,24 +58,51 @@ export class BudaClient {
56
58
  };
57
59
  }
58
60
 
59
- async get<T>(path: string, params?: Record<string, string | number>): Promise<T> {
60
- const url = new URL(`${this.baseUrl}${path}.json`);
61
+ /**
62
+ * Parses the Retry-After header value into milliseconds.
63
+ * Per RFC 7231, Retry-After is an integer number of seconds.
64
+ * Defaults to 1000ms (1 second) if absent or unparseable.
65
+ */
66
+ private parseRetryAfterMs(headers: Headers): number {
67
+ const raw = headers.get("Retry-After");
68
+ if (!raw) return 1000;
69
+ const secs = parseInt(raw, 10);
70
+ return isNaN(secs) ? 1000 : secs * 1000;
71
+ }
61
72
 
62
- if (params) {
63
- for (const [key, value] of Object.entries(params)) {
64
- url.searchParams.set(key, String(value));
65
- }
73
+ /**
74
+ * Executes a fetch call with a single 429 retry.
75
+ * On the first 429, waits for Retry-After seconds (default 1s), then retries once.
76
+ * If the retry also returns 429, throws a BudaApiError with retryAfterMs set.
77
+ */
78
+ private async fetchWithRetry(
79
+ url: URL,
80
+ options: RequestInit,
81
+ path: string,
82
+ ): Promise<Response> {
83
+ const response = await fetch(url.toString(), options);
84
+
85
+ if (response.status !== 429) return response;
86
+
87
+ const retryAfterMs = this.parseRetryAfterMs(response.headers);
88
+ await new Promise((r) => setTimeout(r, retryAfterMs));
89
+
90
+ const retry = await fetch(url.toString(), options);
91
+
92
+ if (retry.status === 429) {
93
+ const retryAgainMs = this.parseRetryAfterMs(retry.headers);
94
+ throw new BudaApiError(
95
+ 429,
96
+ path,
97
+ `Buda API rate limit exceeded. Retry after ${retryAgainMs}ms.`,
98
+ retryAgainMs,
99
+ );
66
100
  }
67
101
 
68
- const urlPath = url.pathname + url.search;
69
- const headers: Record<string, string> = {
70
- Accept: "application/json",
71
- "User-Agent": "buda-mcp/1.1.0",
72
- ...this.authHeaders("GET", urlPath),
73
- };
74
-
75
- const response = await fetch(url.toString(), { headers });
102
+ return retry;
103
+ }
76
104
 
105
+ private async handleResponse<T>(response: Response, path: string): Promise<T> {
77
106
  if (!response.ok) {
78
107
  let detail = response.statusText;
79
108
  try {
@@ -84,10 +113,29 @@ export class BudaClient {
84
113
  }
85
114
  throw new BudaApiError(response.status, path, `Buda API ${response.status}: ${detail}`);
86
115
  }
87
-
88
116
  return response.json() as Promise<T>;
89
117
  }
90
118
 
119
+ async get<T>(path: string, params?: Record<string, string | number>): Promise<T> {
120
+ const url = new URL(`${this.baseUrl}${path}.json`);
121
+
122
+ if (params) {
123
+ for (const [key, value] of Object.entries(params)) {
124
+ url.searchParams.set(key, String(value));
125
+ }
126
+ }
127
+
128
+ const urlPath = url.pathname + url.search;
129
+ const headers: Record<string, string> = {
130
+ Accept: "application/json",
131
+ "User-Agent": `buda-mcp/${VERSION}`,
132
+ ...this.authHeaders("GET", urlPath),
133
+ };
134
+
135
+ const response = await this.fetchWithRetry(url, { headers }, path);
136
+ return this.handleResponse<T>(response, path);
137
+ }
138
+
91
139
  async post<T>(path: string, payload: unknown): Promise<T> {
92
140
  const url = new URL(`${this.baseUrl}${path}.json`);
93
141
  const bodyStr = JSON.stringify(payload);
@@ -95,28 +143,16 @@ export class BudaClient {
95
143
  const headers: Record<string, string> = {
96
144
  Accept: "application/json",
97
145
  "Content-Type": "application/json",
98
- "User-Agent": "buda-mcp/1.1.0",
146
+ "User-Agent": `buda-mcp/${VERSION}`,
99
147
  ...this.authHeaders("POST", urlPath, bodyStr),
100
148
  };
101
149
 
102
- const response = await fetch(url.toString(), {
103
- method: "POST",
104
- headers,
105
- body: bodyStr,
106
- });
107
-
108
- if (!response.ok) {
109
- let detail = response.statusText;
110
- try {
111
- const body = (await response.json()) as { message?: string };
112
- if (body.message) detail = body.message;
113
- } catch {
114
- // ignore parse error, use statusText
115
- }
116
- throw new BudaApiError(response.status, path, `Buda API ${response.status}: ${detail}`);
117
- }
118
-
119
- return response.json() as Promise<T>;
150
+ const response = await this.fetchWithRetry(
151
+ url,
152
+ { method: "POST", headers, body: bodyStr },
153
+ path,
154
+ );
155
+ return this.handleResponse<T>(response, path);
120
156
  }
121
157
 
122
158
  async put<T>(path: string, payload: unknown): Promise<T> {
@@ -126,27 +162,15 @@ export class BudaClient {
126
162
  const headers: Record<string, string> = {
127
163
  Accept: "application/json",
128
164
  "Content-Type": "application/json",
129
- "User-Agent": "buda-mcp/1.1.0",
165
+ "User-Agent": `buda-mcp/${VERSION}`,
130
166
  ...this.authHeaders("PUT", urlPath, bodyStr),
131
167
  };
132
168
 
133
- const response = await fetch(url.toString(), {
134
- method: "PUT",
135
- headers,
136
- body: bodyStr,
137
- });
138
-
139
- if (!response.ok) {
140
- let detail = response.statusText;
141
- try {
142
- const body = (await response.json()) as { message?: string };
143
- if (body.message) detail = body.message;
144
- } catch {
145
- // ignore parse error, use statusText
146
- }
147
- throw new BudaApiError(response.status, path, `Buda API ${response.status}: ${detail}`);
148
- }
149
-
150
- return response.json() as Promise<T>;
169
+ const response = await this.fetchWithRetry(
170
+ url,
171
+ { method: "PUT", headers, body: bodyStr },
172
+ path,
173
+ );
174
+ return this.handleResponse<T>(response, path);
151
175
  }
152
176
  }
package/src/http.ts CHANGED
@@ -3,6 +3,7 @@ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mc
3
3
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
4
  import { BudaClient } from "./client.js";
5
5
  import { MemoryCache, CACHE_TTL } from "./cache.js";
6
+ import { VERSION } from "./version.js";
6
7
  import type { MarketsResponse, TickerResponse } from "./types.js";
7
8
  import * as markets from "./tools/markets.js";
8
9
  import * as ticker from "./tools/ticker.js";
@@ -27,8 +28,28 @@ const client = new BudaClient(
27
28
 
28
29
  const authEnabled = client.hasAuth();
29
30
 
31
+ // Schemas for the Smithery server-card — assembled from the same definitions used in register().
32
+ // Adding a new tool only requires exporting its toolSchema; no changes needed here.
33
+ const PUBLIC_TOOL_SCHEMAS = [
34
+ markets.toolSchema,
35
+ ticker.toolSchema,
36
+ orderbook.toolSchema,
37
+ trades.toolSchema,
38
+ volume.toolSchema,
39
+ spread.toolSchema,
40
+ compareMarkets.toolSchema,
41
+ priceHistory.toolSchema,
42
+ ];
43
+
44
+ const AUTH_TOOL_SCHEMAS = [
45
+ balances.toolSchema,
46
+ orders.toolSchema,
47
+ placeOrder.toolSchema,
48
+ cancelOrder.toolSchema,
49
+ ];
50
+
30
51
  function createServer(): McpServer {
31
- const server = new McpServer({ name: "buda-mcp", version: "1.1.1" });
52
+ const server = new McpServer({ name: "buda-mcp", version: VERSION });
32
53
 
33
54
  // Per-request cache so caching works correctly for stateless HTTP
34
55
  const reqCache = new MemoryCache();
@@ -101,160 +122,21 @@ app.use(express.json());
101
122
 
102
123
  // Health check for Railway / uptime monitors
103
124
  app.get("/health", (_req, res) => {
104
- res.json({ status: "ok", server: "buda-mcp", version: "1.1.1", auth_mode: authEnabled ? "authenticated" : "public" });
125
+ res.json({
126
+ status: "ok",
127
+ server: "buda-mcp",
128
+ version: VERSION,
129
+ auth_mode: authEnabled ? "authenticated" : "public",
130
+ });
105
131
  });
106
132
 
107
- // Smithery static server card — lets Smithery scan tools without running the server
133
+ // Smithery static server card — assembled programmatically from tool definitions.
134
+ // Adding a new tool only requires exporting its toolSchema; this handler needs no changes.
108
135
  app.get("/.well-known/mcp/server-card.json", (_req, res) => {
109
- const publicTools = [
110
- {
111
- name: "get_markets",
112
- description: "List all available trading pairs on Buda.com, or get details for a specific market.",
113
- inputSchema: {
114
- type: "object",
115
- properties: {
116
- market_id: { type: "string", description: "Optional market ID (e.g. BTC-CLP)" },
117
- },
118
- },
119
- },
120
- {
121
- name: "get_ticker",
122
- description: "Get current price, bid/ask, volume, and price change for a Buda.com market.",
123
- inputSchema: {
124
- type: "object",
125
- properties: {
126
- market_id: { type: "string", description: "Market ID (e.g. BTC-CLP)" },
127
- },
128
- required: ["market_id"],
129
- },
130
- },
131
- {
132
- name: "get_orderbook",
133
- description: "Get the full order book (bids and asks) for a Buda.com market.",
134
- inputSchema: {
135
- type: "object",
136
- properties: {
137
- market_id: { type: "string", description: "Market ID (e.g. BTC-CLP)" },
138
- limit: { type: "number", description: "Max levels per side" },
139
- },
140
- required: ["market_id"],
141
- },
142
- },
143
- {
144
- name: "get_trades",
145
- description: "Get recent trade history for a Buda.com market.",
146
- inputSchema: {
147
- type: "object",
148
- properties: {
149
- market_id: { type: "string", description: "Market ID (e.g. BTC-CLP)" },
150
- limit: { type: "number", description: "Number of trades (max 100)" },
151
- timestamp: { type: "number", description: "Unix timestamp for pagination" },
152
- },
153
- required: ["market_id"],
154
- },
155
- },
156
- {
157
- name: "get_market_volume",
158
- description: "Get 24h and 7-day transacted volume for a Buda.com market.",
159
- inputSchema: {
160
- type: "object",
161
- properties: {
162
- market_id: { type: "string", description: "Market ID (e.g. BTC-CLP)" },
163
- },
164
- required: ["market_id"],
165
- },
166
- },
167
- {
168
- name: "get_spread",
169
- description: "Calculate bid/ask spread (absolute and percentage) for a Buda.com market.",
170
- inputSchema: {
171
- type: "object",
172
- properties: {
173
- market_id: { type: "string", description: "Market ID (e.g. BTC-CLP)" },
174
- },
175
- required: ["market_id"],
176
- },
177
- },
178
- {
179
- name: "compare_markets",
180
- description: "Compare ticker data for all trading pairs of a given base currency side by side.",
181
- inputSchema: {
182
- type: "object",
183
- properties: {
184
- base_currency: { type: "string", description: "Base currency (e.g. BTC, ETH)" },
185
- },
186
- required: ["base_currency"],
187
- },
188
- },
189
- {
190
- name: "get_price_history",
191
- description: "Get OHLCV price history for a market, derived from recent trade history.",
192
- inputSchema: {
193
- type: "object",
194
- properties: {
195
- market_id: { type: "string", description: "Market ID (e.g. BTC-CLP)" },
196
- period: { type: "string", enum: ["1h", "4h", "1d"], description: "Candle period" },
197
- limit: { type: "number", description: "Raw trades to fetch (max 100)" },
198
- },
199
- required: ["market_id"],
200
- },
201
- },
202
- ];
203
-
204
- const authTools = authEnabled
205
- ? [
206
- {
207
- name: "get_balances",
208
- description: "Get all currency balances for the authenticated account.",
209
- inputSchema: { type: "object", properties: {} },
210
- },
211
- {
212
- name: "get_orders",
213
- description: "Get orders for a given market.",
214
- inputSchema: {
215
- type: "object",
216
- properties: {
217
- market_id: { type: "string" },
218
- state: { type: "string" },
219
- },
220
- required: ["market_id"],
221
- },
222
- },
223
- {
224
- name: "place_order",
225
- description: "Place a limit or market order. Requires confirmation_token='CONFIRM'.",
226
- inputSchema: {
227
- type: "object",
228
- properties: {
229
- market_id: { type: "string" },
230
- type: { type: "string", enum: ["Bid", "Ask"] },
231
- price_type: { type: "string", enum: ["limit", "market"] },
232
- amount: { type: "number" },
233
- limit_price: { type: "number" },
234
- confirmation_token: { type: "string" },
235
- },
236
- required: ["market_id", "type", "price_type", "amount", "confirmation_token"],
237
- },
238
- },
239
- {
240
- name: "cancel_order",
241
- description: "Cancel an order by ID. Requires confirmation_token='CONFIRM'.",
242
- inputSchema: {
243
- type: "object",
244
- properties: {
245
- order_id: { type: "number" },
246
- confirmation_token: { type: "string" },
247
- },
248
- required: ["order_id", "confirmation_token"],
249
- },
250
- },
251
- ]
252
- : [];
253
-
254
136
  res.json({
255
- serverInfo: { name: "buda-mcp", version: "1.1.1" },
137
+ serverInfo: { name: "buda-mcp", version: VERSION },
256
138
  authentication: { required: authEnabled },
257
- tools: [...publicTools, ...authTools],
139
+ tools: [...PUBLIC_TOOL_SCHEMAS, ...(authEnabled ? AUTH_TOOL_SCHEMAS : [])],
258
140
  resources: [
259
141
  { uri: "buda://markets", name: "All Buda.com markets", mimeType: "application/json" },
260
142
  { uri: "buda://ticker/{market}", name: "Ticker for a specific market", mimeType: "application/json" },
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
3
3
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
4
4
  import { BudaClient } from "./client.js";
5
5
  import { cache, CACHE_TTL } from "./cache.js";
6
+ import { VERSION } from "./version.js";
6
7
  import type { MarketsResponse, TickerResponse } from "./types.js";
7
8
  import * as markets from "./tools/markets.js";
8
9
  import * as ticker from "./tools/ticker.js";
@@ -25,7 +26,7 @@ const client = new BudaClient(
25
26
 
26
27
  const server = new McpServer({
27
28
  name: "buda-mcp",
28
- version: "1.1.1",
29
+ version: VERSION,
29
30
  });
30
31
 
31
32
  // Public tools
@@ -2,12 +2,22 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { BudaClient, BudaApiError } from "../client.js";
3
3
  import type { BalancesResponse } from "../types.js";
4
4
 
5
+ export const toolSchema = {
6
+ name: "get_balances",
7
+ description:
8
+ "Get all currency balances for the authenticated Buda.com account. " +
9
+ "Returns total, available, frozen, and pending withdrawal amounts per currency. " +
10
+ "Requires BUDA_API_KEY and BUDA_API_SECRET environment variables.",
11
+ inputSchema: {
12
+ type: "object" as const,
13
+ properties: {},
14
+ },
15
+ };
16
+
5
17
  export function register(server: McpServer, client: BudaClient): void {
6
18
  server.tool(
7
- "get_balances",
8
- "Get all currency balances for the authenticated Buda.com account. " +
9
- "Returns total, available, frozen, and pending withdrawal amounts per currency. " +
10
- "Requires BUDA_API_KEY and BUDA_API_SECRET environment variables.",
19
+ toolSchema.name,
20
+ toolSchema.description,
11
21
  {},
12
22
  async () => {
13
23
  try {
@@ -3,13 +3,84 @@ import { z } from "zod";
3
3
  import { BudaClient, BudaApiError } from "../client.js";
4
4
  import type { OrderResponse } from "../types.js";
5
5
 
6
+ export const toolSchema = {
7
+ name: "cancel_order",
8
+ description:
9
+ "Cancel an open order by ID on Buda.com. " +
10
+ "IMPORTANT: To prevent accidental cancellation from ambiguous prompts, you must pass " +
11
+ "confirmation_token='CONFIRM' to execute. " +
12
+ "Requires BUDA_API_KEY and BUDA_API_SECRET environment variables.",
13
+ inputSchema: {
14
+ type: "object" as const,
15
+ properties: {
16
+ order_id: {
17
+ type: "number",
18
+ description: "The numeric ID of the order to cancel.",
19
+ },
20
+ confirmation_token: {
21
+ type: "string",
22
+ description:
23
+ "Safety confirmation. Must equal exactly 'CONFIRM' (case-sensitive) to cancel the order. " +
24
+ "Any other value will reject the request without canceling.",
25
+ },
26
+ },
27
+ required: ["order_id", "confirmation_token"],
28
+ },
29
+ };
30
+
31
+ type CancelOrderArgs = {
32
+ order_id: number;
33
+ confirmation_token: string;
34
+ };
35
+
36
+ export async function handleCancelOrder(
37
+ args: CancelOrderArgs,
38
+ client: BudaClient,
39
+ ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
40
+ const { order_id, confirmation_token } = args;
41
+
42
+ if (confirmation_token !== "CONFIRM") {
43
+ return {
44
+ content: [
45
+ {
46
+ type: "text",
47
+ text: JSON.stringify({
48
+ error:
49
+ "Order not canceled. confirmation_token must equal 'CONFIRM' to execute. " +
50
+ "Verify the order ID and set confirmation_token='CONFIRM' to proceed.",
51
+ code: "CONFIRMATION_REQUIRED",
52
+ order_id,
53
+ }),
54
+ },
55
+ ],
56
+ isError: true,
57
+ };
58
+ }
59
+
60
+ try {
61
+ const data = await client.put<OrderResponse>(`/orders/${order_id}`, {
62
+ state: "canceling",
63
+ });
64
+
65
+ return {
66
+ content: [{ type: "text", text: JSON.stringify(data.order, null, 2) }],
67
+ };
68
+ } catch (err) {
69
+ const msg =
70
+ err instanceof BudaApiError
71
+ ? { error: err.message, code: err.status, path: err.path }
72
+ : { error: String(err), code: "UNKNOWN" };
73
+ return {
74
+ content: [{ type: "text", text: JSON.stringify(msg) }],
75
+ isError: true,
76
+ };
77
+ }
78
+ }
79
+
6
80
  export function register(server: McpServer, client: BudaClient): void {
7
81
  server.tool(
8
- "cancel_order",
9
- "Cancel an open order by ID on Buda.com. " +
10
- "IMPORTANT: To prevent accidental cancellation from ambiguous prompts, you must pass " +
11
- "confirmation_token='CONFIRM' to execute. " +
12
- "Requires BUDA_API_KEY and BUDA_API_SECRET environment variables.",
82
+ toolSchema.name,
83
+ toolSchema.description,
13
84
  {
14
85
  order_id: z
15
86
  .number()
@@ -23,43 +94,6 @@ export function register(server: McpServer, client: BudaClient): void {
23
94
  "Any other value will reject the request without canceling.",
24
95
  ),
25
96
  },
26
- async ({ order_id, confirmation_token }) => {
27
- if (confirmation_token !== "CONFIRM") {
28
- return {
29
- content: [
30
- {
31
- type: "text",
32
- text: JSON.stringify({
33
- error:
34
- "Order not canceled. confirmation_token must equal 'CONFIRM' to execute. " +
35
- "Verify the order ID and set confirmation_token='CONFIRM' to proceed.",
36
- code: "CONFIRMATION_REQUIRED",
37
- order_id,
38
- }),
39
- },
40
- ],
41
- isError: true,
42
- };
43
- }
44
-
45
- try {
46
- const data = await client.put<OrderResponse>(`/orders/${order_id}`, {
47
- state: "canceling",
48
- });
49
-
50
- return {
51
- content: [{ type: "text", text: JSON.stringify(data.order, null, 2) }],
52
- };
53
- } catch (err) {
54
- const msg =
55
- err instanceof BudaApiError
56
- ? { error: err.message, code: err.status, path: err.path }
57
- : { error: String(err), code: "UNKNOWN" };
58
- return {
59
- content: [{ type: "text", text: JSON.stringify(msg) }],
60
- isError: true,
61
- };
62
- }
63
- },
97
+ (args) => handleCancelOrder(args, client),
64
98
  );
65
99
  }
@@ -4,12 +4,29 @@ import { BudaClient, BudaApiError } from "../client.js";
4
4
  import { MemoryCache, CACHE_TTL } from "../cache.js";
5
5
  import type { AllTickersResponse } from "../types.js";
6
6
 
7
+ export const toolSchema = {
8
+ name: "compare_markets",
9
+ description:
10
+ "Compare ticker data for all trading pairs of a given base currency across Buda.com's " +
11
+ "supported quote currencies (CLP, COP, PEN, BTC, USDC, ETH). " +
12
+ "For example, passing 'BTC' returns side-by-side data for BTC-CLP, BTC-COP, BTC-PEN, etc.",
13
+ inputSchema: {
14
+ type: "object" as const,
15
+ properties: {
16
+ base_currency: {
17
+ type: "string",
18
+ description:
19
+ "Base currency to compare across all available markets (e.g. 'BTC', 'ETH', 'XRP').",
20
+ },
21
+ },
22
+ required: ["base_currency"],
23
+ },
24
+ };
25
+
7
26
  export function register(server: McpServer, client: BudaClient, cache: MemoryCache): void {
8
27
  server.tool(
9
- "compare_markets",
10
- "Compare ticker data for all trading pairs of a given base currency across Buda.com's " +
11
- "supported quote currencies (CLP, COP, PEN, BTC, USDC, ETH). " +
12
- "For example, passing 'BTC' returns side-by-side data for BTC-CLP, BTC-COP, BTC-PEN, etc.",
28
+ toolSchema.name,
29
+ toolSchema.description,
13
30
  {
14
31
  base_currency: z
15
32
  .string()
@@ -2,13 +2,29 @@ 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 { MemoryCache, CACHE_TTL } from "../cache.js";
5
+ import { validateMarketId } from "../validation.js";
5
6
  import type { MarketsResponse, MarketResponse } from "../types.js";
6
7
 
8
+ export const toolSchema = {
9
+ name: "get_markets",
10
+ description:
11
+ "List all available trading pairs on Buda.com, or get details for a specific market. " +
12
+ "Returns base/quote currencies, fees, and minimum order sizes.",
13
+ inputSchema: {
14
+ type: "object" as const,
15
+ properties: {
16
+ market_id: {
17
+ type: "string",
18
+ description: "Optional market ID (e.g. 'BTC-CLP', 'ETH-BTC'). Omit to list all markets.",
19
+ },
20
+ },
21
+ },
22
+ };
23
+
7
24
  export function register(server: McpServer, client: BudaClient, cache: MemoryCache): void {
8
25
  server.tool(
9
- "get_markets",
10
- "List all available trading pairs on Buda.com, or get details for a specific market. " +
11
- "Returns base/quote currencies, fees, and minimum order sizes.",
26
+ toolSchema.name,
27
+ toolSchema.description,
12
28
  {
13
29
  market_id: z
14
30
  .string()
@@ -20,6 +36,14 @@ export function register(server: McpServer, client: BudaClient, cache: MemoryCac
20
36
  async ({ market_id }) => {
21
37
  try {
22
38
  if (market_id) {
39
+ const validationError = validateMarketId(market_id);
40
+ if (validationError) {
41
+ return {
42
+ content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_MARKET_ID" }) }],
43
+ isError: true,
44
+ };
45
+ }
46
+
23
47
  const id = market_id.toLowerCase();
24
48
  const data = await cache.getOrFetch<MarketResponse>(
25
49
  `market:${id}`,