@guiie/buda-mcp 1.1.2 → 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.
@@ -1,16 +1,143 @@
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 { validateMarketId } from "../validation.js";
4
5
  import type { OrderResponse } from "../types.js";
5
6
 
7
+ export const toolSchema = {
8
+ name: "place_order",
9
+ description:
10
+ "Place a limit or market order on Buda.com. " +
11
+ "IMPORTANT: To prevent accidental execution from ambiguous prompts, you must pass " +
12
+ "confirmation_token='CONFIRM' to execute the order. " +
13
+ "Requires BUDA_API_KEY and BUDA_API_SECRET environment variables. " +
14
+ "WARNING: Only use this tool on a locally-run instance — never on a publicly exposed server.",
15
+ inputSchema: {
16
+ type: "object" as const,
17
+ properties: {
18
+ market_id: {
19
+ type: "string",
20
+ description: "Market ID (e.g. 'BTC-CLP', 'ETH-BTC').",
21
+ },
22
+ type: {
23
+ type: "string",
24
+ description: "Order side: 'Bid' to buy, 'Ask' to sell.",
25
+ },
26
+ price_type: {
27
+ type: "string",
28
+ description: "Order type: 'limit' places at a specific price, 'market' executes immediately.",
29
+ },
30
+ amount: {
31
+ type: "number",
32
+ description: "Order size in the market's base currency (e.g. BTC amount for BTC-CLP).",
33
+ },
34
+ limit_price: {
35
+ type: "number",
36
+ description:
37
+ "Limit price in quote currency. Required when price_type is 'limit'. " +
38
+ "For Bid orders: highest price you will pay. For Ask orders: lowest price you will accept.",
39
+ },
40
+ confirmation_token: {
41
+ type: "string",
42
+ description:
43
+ "Safety confirmation. Must equal exactly 'CONFIRM' (case-sensitive) to execute the order. " +
44
+ "Any other value will reject the request without placing an order.",
45
+ },
46
+ },
47
+ required: ["market_id", "type", "price_type", "amount", "confirmation_token"],
48
+ },
49
+ };
50
+
51
+ type PlaceOrderArgs = {
52
+ market_id: string;
53
+ type: "Bid" | "Ask";
54
+ price_type: "limit" | "market";
55
+ amount: number;
56
+ limit_price?: number;
57
+ confirmation_token: string;
58
+ };
59
+
60
+ export async function handlePlaceOrder(
61
+ args: PlaceOrderArgs,
62
+ client: BudaClient,
63
+ ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
64
+ const { market_id, type, price_type, amount, limit_price, confirmation_token } = args;
65
+
66
+ if (confirmation_token !== "CONFIRM") {
67
+ return {
68
+ content: [
69
+ {
70
+ type: "text",
71
+ text: JSON.stringify({
72
+ error:
73
+ "Order not placed. confirmation_token must equal 'CONFIRM' to execute. " +
74
+ "Review the order details and set confirmation_token='CONFIRM' to proceed.",
75
+ code: "CONFIRMATION_REQUIRED",
76
+ order_preview: { market_id, type, price_type, amount, limit_price },
77
+ }),
78
+ },
79
+ ],
80
+ isError: true,
81
+ };
82
+ }
83
+
84
+ const validationError = validateMarketId(market_id);
85
+ if (validationError) {
86
+ return {
87
+ content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_MARKET_ID" }) }],
88
+ isError: true,
89
+ };
90
+ }
91
+
92
+ try {
93
+ const payload: Record<string, unknown> = {
94
+ type,
95
+ price_type,
96
+ amount,
97
+ };
98
+
99
+ if (price_type === "limit") {
100
+ if (limit_price === undefined) {
101
+ return {
102
+ content: [
103
+ {
104
+ type: "text",
105
+ text: JSON.stringify({
106
+ error: "limit_price is required when price_type is 'limit'.",
107
+ code: "VALIDATION_ERROR",
108
+ }),
109
+ },
110
+ ],
111
+ isError: true,
112
+ };
113
+ }
114
+ payload.limit = { price: limit_price, type: "gtc" };
115
+ }
116
+
117
+ const data = await client.post<OrderResponse>(
118
+ `/markets/${market_id.toLowerCase()}/orders`,
119
+ payload,
120
+ );
121
+
122
+ return {
123
+ content: [{ type: "text", text: JSON.stringify(data.order, null, 2) }],
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
+
6
137
  export function register(server: McpServer, client: BudaClient): void {
7
138
  server.tool(
8
- "place_order",
9
- "Place a limit or market order on Buda.com. " +
10
- "IMPORTANT: To prevent accidental execution from ambiguous prompts, you must pass " +
11
- "confirmation_token='CONFIRM' to execute the order. " +
12
- "Requires BUDA_API_KEY and BUDA_API_SECRET environment variables. " +
13
- "WARNING: Only use this tool on a locally-run instance — never on a publicly exposed server.",
139
+ toolSchema.name,
140
+ toolSchema.description,
14
141
  {
15
142
  market_id: z
16
143
  .string()
@@ -40,68 +167,6 @@ export function register(server: McpServer, client: BudaClient): void {
40
167
  "Any other value will reject the request without placing an order.",
41
168
  ),
42
169
  },
43
- async ({ market_id, type, price_type, amount, limit_price, confirmation_token }) => {
44
- if (confirmation_token !== "CONFIRM") {
45
- return {
46
- content: [
47
- {
48
- type: "text",
49
- text: JSON.stringify({
50
- error:
51
- "Order not placed. confirmation_token must equal 'CONFIRM' to execute. " +
52
- "Review the order details and set confirmation_token='CONFIRM' to proceed.",
53
- code: "CONFIRMATION_REQUIRED",
54
- order_preview: { market_id, type, price_type, amount, limit_price },
55
- }),
56
- },
57
- ],
58
- isError: true,
59
- };
60
- }
61
-
62
- try {
63
- const payload: Record<string, unknown> = {
64
- type,
65
- price_type,
66
- amount,
67
- };
68
-
69
- if (price_type === "limit") {
70
- if (limit_price === undefined) {
71
- return {
72
- content: [
73
- {
74
- type: "text",
75
- text: JSON.stringify({
76
- error: "limit_price is required when price_type is 'limit'.",
77
- code: "VALIDATION_ERROR",
78
- }),
79
- },
80
- ],
81
- isError: true,
82
- };
83
- }
84
- payload.limit = { price: limit_price, type: "gtc" };
85
- }
86
-
87
- const data = await client.post<OrderResponse>(
88
- `/markets/${market_id.toLowerCase()}/orders`,
89
- payload,
90
- );
91
-
92
- return {
93
- content: [{ type: "text", text: JSON.stringify(data.order, null, 2) }],
94
- };
95
- } catch (err) {
96
- const msg =
97
- err instanceof BudaApiError
98
- ? { error: err.message, code: err.status, path: err.path }
99
- : { error: String(err), code: "UNKNOWN" };
100
- return {
101
- content: [{ type: "text", text: JSON.stringify(msg) }],
102
- isError: true,
103
- };
104
- }
105
- },
170
+ (args) => handlePlaceOrder(args, client),
106
171
  );
107
172
  }
@@ -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 { MemoryCache } from "../cache.js";
5
+ import { validateMarketId } from "../validation.js";
5
6
  import type { TradesResponse } from "../types.js";
6
7
 
7
8
  const PERIOD_MS: Record<string, number> = {
@@ -20,13 +21,40 @@ interface OhlcvCandle {
20
21
  trade_count: number;
21
22
  }
22
23
 
24
+ export const toolSchema = {
25
+ name: "get_price_history",
26
+ description:
27
+ "IMPORTANT: Candles are aggregated client-side from raw trades (Buda has no native candlestick " +
28
+ "endpoint) — fetching more trades via the 'limit' parameter gives deeper history but slower " +
29
+ "responses. Returns OHLCV (open/high/low/close/volume) price history for a Buda.com market. " +
30
+ "Candle timestamps are UTC bucket boundaries (e.g. '2026-04-10T12:00:00.000Z' for 1h). " +
31
+ "Supports 1h, 4h, and 1d candle periods.",
32
+ inputSchema: {
33
+ type: "object" as const,
34
+ properties: {
35
+ market_id: {
36
+ type: "string",
37
+ description: "Market ID (e.g. 'BTC-CLP', 'ETH-BTC').",
38
+ },
39
+ period: {
40
+ type: "string",
41
+ description: "Candle period: '1h' (1 hour), '4h' (4 hours), or '1d' (1 day). Default: '1h'.",
42
+ },
43
+ limit: {
44
+ type: "number",
45
+ description:
46
+ "Raw trades to fetch before aggregation (default: 100, max: 1000). " +
47
+ "More trades = deeper history but slower response.",
48
+ },
49
+ },
50
+ required: ["market_id"],
51
+ },
52
+ };
53
+
23
54
  export function register(server: McpServer, client: BudaClient, _cache: MemoryCache): void {
24
55
  server.tool(
25
- "get_price_history",
26
- "Get OHLCV (open/high/low/close/volume) price history for a Buda.com market, derived from " +
27
- "recent trade history (up to 100 trades). Buda does not provide a native candlestick endpoint; " +
28
- "candles are aggregated client-side from raw trades. Use the 'period' parameter to control " +
29
- "candle size (1h, 4h, or 1d).",
56
+ toolSchema.name,
57
+ toolSchema.description,
30
58
  {
31
59
  market_id: z
32
60
  .string()
@@ -39,12 +67,23 @@ export function register(server: McpServer, client: BudaClient, _cache: MemoryCa
39
67
  .number()
40
68
  .int()
41
69
  .min(1)
42
- .max(100)
70
+ .max(1000)
43
71
  .optional()
44
- .describe("Number of raw trades to fetch before aggregation (default: 100, max: 100)."),
72
+ .describe(
73
+ "Raw trades to fetch before aggregation (default: 100, max: 1000). " +
74
+ "More trades = deeper history but slower response.",
75
+ ),
45
76
  },
46
77
  async ({ market_id, period, limit }) => {
47
78
  try {
79
+ const validationError = validateMarketId(market_id);
80
+ if (validationError) {
81
+ return {
82
+ content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_MARKET_ID" }) }],
83
+ isError: true,
84
+ };
85
+ }
86
+
48
87
  const id = market_id.toLowerCase();
49
88
  const tradesLimit = limit ?? 100;
50
89
 
@@ -108,7 +147,10 @@ export function register(server: McpServer, client: BudaClient, _cache: MemoryCa
108
147
  market_id: market_id.toUpperCase(),
109
148
  period,
110
149
  candle_count: candles.length,
111
- note: "Candles derived from trade history (up to 100 trades). For deeper history, use pagination via the get_trades tool.",
150
+ trades_fetched: entries.length,
151
+ note:
152
+ "Candles derived from raw trade history. Candle timestamps are UTC bucket boundaries. " +
153
+ "Increase 'limit' (max 1000) for deeper history.",
112
154
  candles,
113
155
  };
114
156
 
@@ -2,13 +2,30 @@ 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 { TickerResponse } from "../types.js";
6
7
 
8
+ export const toolSchema = {
9
+ name: "get_spread",
10
+ description:
11
+ "Calculate the bid/ask spread for a Buda.com market. " +
12
+ "Returns the best bid, best ask, absolute spread, and spread as a percentage of the ask price.",
13
+ inputSchema: {
14
+ type: "object" as const,
15
+ properties: {
16
+ market_id: {
17
+ type: "string",
18
+ description: "Market ID (e.g. 'BTC-CLP', 'ETH-BTC', 'BTC-COP').",
19
+ },
20
+ },
21
+ required: ["market_id"],
22
+ },
23
+ };
24
+
7
25
  export function register(server: McpServer, client: BudaClient, cache: MemoryCache): void {
8
26
  server.tool(
9
- "get_spread",
10
- "Calculate the bid/ask spread for a Buda.com market. " +
11
- "Returns the best bid, best ask, absolute spread, and spread as a percentage of the ask price.",
27
+ toolSchema.name,
28
+ toolSchema.description,
12
29
  {
13
30
  market_id: z
14
31
  .string()
@@ -16,6 +33,14 @@ export function register(server: McpServer, client: BudaClient, cache: MemoryCac
16
33
  },
17
34
  async ({ market_id }) => {
18
35
  try {
36
+ const validationError = validateMarketId(market_id);
37
+ if (validationError) {
38
+ return {
39
+ content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_MARKET_ID" }) }],
40
+ isError: true,
41
+ };
42
+ }
43
+
19
44
  const id = market_id.toLowerCase();
20
45
  const data = await cache.getOrFetch<TickerResponse>(
21
46
  `ticker:${id}`,
@@ -2,13 +2,30 @@ 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 { TickerResponse } from "../types.js";
6
7
 
8
+ export const toolSchema = {
9
+ name: "get_ticker",
10
+ description:
11
+ "Get the current ticker for a Buda.com market: last traded price, best bid/ask, " +
12
+ "24h volume, and price change over 24h and 7d.",
13
+ inputSchema: {
14
+ type: "object" as const,
15
+ properties: {
16
+ market_id: {
17
+ type: "string",
18
+ description: "Market ID (e.g. 'BTC-CLP', 'ETH-BTC', 'BTC-COP').",
19
+ },
20
+ },
21
+ required: ["market_id"],
22
+ },
23
+ };
24
+
7
25
  export function register(server: McpServer, client: BudaClient, cache: MemoryCache): void {
8
26
  server.tool(
9
- "get_ticker",
10
- "Get the current ticker for a Buda.com market: last traded price, best bid/ask, " +
11
- "24h volume, and price change over 24h and 7d.",
27
+ toolSchema.name,
28
+ toolSchema.description,
12
29
  {
13
30
  market_id: z
14
31
  .string()
@@ -16,6 +33,14 @@ export function register(server: McpServer, client: BudaClient, cache: MemoryCac
16
33
  },
17
34
  async ({ market_id }) => {
18
35
  try {
36
+ const validationError = validateMarketId(market_id);
37
+ if (validationError) {
38
+ return {
39
+ content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_MARKET_ID" }) }],
40
+ isError: true,
41
+ };
42
+ }
43
+
19
44
  const id = market_id.toLowerCase();
20
45
  const data = await cache.getOrFetch<TickerResponse>(
21
46
  `ticker:${id}`,
@@ -2,13 +2,39 @@ 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 } from "../cache.js";
5
+ import { validateMarketId } from "../validation.js";
5
6
  import type { TradesResponse } from "../types.js";
6
7
 
8
+ export const toolSchema = {
9
+ name: "get_trades",
10
+ description:
11
+ "Get recent trade history for a Buda.com market. Each entry contains " +
12
+ "[timestamp_ms, amount, price, direction]. Direction is 'buy' or 'sell'.",
13
+ inputSchema: {
14
+ type: "object" as const,
15
+ properties: {
16
+ market_id: {
17
+ type: "string",
18
+ description: "Market ID (e.g. 'BTC-CLP', 'ETH-BTC').",
19
+ },
20
+ limit: {
21
+ type: "number",
22
+ description: "Number of trades to return (default: 50, max: 100).",
23
+ },
24
+ timestamp: {
25
+ type: "number",
26
+ description:
27
+ "Unix timestamp (seconds) to paginate from. Returns trades older than this timestamp.",
28
+ },
29
+ },
30
+ required: ["market_id"],
31
+ },
32
+ };
33
+
7
34
  export function register(server: McpServer, client: BudaClient, _cache: MemoryCache): void {
8
35
  server.tool(
9
- "get_trades",
10
- "Get recent trade history for a Buda.com market. Each entry contains " +
11
- "[timestamp_ms, amount, price, direction]. Direction is 'buy' or 'sell'.",
36
+ toolSchema.name,
37
+ toolSchema.description,
12
38
  {
13
39
  market_id: z
14
40
  .string()
@@ -30,6 +56,14 @@ export function register(server: McpServer, client: BudaClient, _cache: MemoryCa
30
56
  },
31
57
  async ({ market_id, limit, timestamp }) => {
32
58
  try {
59
+ const validationError = validateMarketId(market_id);
60
+ if (validationError) {
61
+ return {
62
+ content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_MARKET_ID" }) }],
63
+ isError: true,
64
+ };
65
+ }
66
+
33
67
  const params: Record<string, string | number> = {};
34
68
  if (limit !== undefined) params.limit = limit;
35
69
  if (timestamp !== undefined) params.timestamp = timestamp;
@@ -2,13 +2,30 @@ 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 } from "../cache.js";
5
+ import { validateMarketId } from "../validation.js";
5
6
  import type { VolumeResponse } from "../types.js";
6
7
 
8
+ export const toolSchema = {
9
+ name: "get_market_volume",
10
+ description:
11
+ "Get 24h and 7-day transacted volume for a Buda.com market. " +
12
+ "Returns ask (sell) and bid (buy) volumes in the market's base currency.",
13
+ inputSchema: {
14
+ type: "object" as const,
15
+ properties: {
16
+ market_id: {
17
+ type: "string",
18
+ description: "Market ID (e.g. 'BTC-CLP', 'ETH-BTC').",
19
+ },
20
+ },
21
+ required: ["market_id"],
22
+ },
23
+ };
24
+
7
25
  export function register(server: McpServer, client: BudaClient, _cache: MemoryCache): void {
8
26
  server.tool(
9
- "get_market_volume",
10
- "Get 24h and 7-day transacted volume for a Buda.com market. " +
11
- "Returns ask (sell) and bid (buy) volumes in the market's base currency.",
27
+ toolSchema.name,
28
+ toolSchema.description,
12
29
  {
13
30
  market_id: z
14
31
  .string()
@@ -16,6 +33,14 @@ export function register(server: McpServer, client: BudaClient, _cache: MemoryCa
16
33
  },
17
34
  async ({ market_id }) => {
18
35
  try {
36
+ const validationError = validateMarketId(market_id);
37
+ if (validationError) {
38
+ return {
39
+ content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_MARKET_ID" }) }],
40
+ isError: true,
41
+ };
42
+ }
43
+
19
44
  const data = await client.get<VolumeResponse>(
20
45
  `/markets/${market_id.toLowerCase()}/volume`,
21
46
  );
@@ -0,0 +1,16 @@
1
+ const MARKET_ID_RE = /^[A-Z0-9]{2,10}-[A-Z0-9]{2,10}$/i;
2
+
3
+ /**
4
+ * Validates a market ID against the expected BASE-QUOTE format.
5
+ * Returns an error message string if invalid, or null if valid.
6
+ */
7
+ export function validateMarketId(id: string): string | null {
8
+ if (!MARKET_ID_RE.test(id)) {
9
+ return (
10
+ `Invalid market ID "${id}". ` +
11
+ `Expected format: BASE-QUOTE with 2–10 alphanumeric characters per part ` +
12
+ `(e.g. "BTC-CLP", "ETH-BTC").`
13
+ );
14
+ }
15
+ return null;
16
+ }
package/src/version.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { readFileSync } from "fs";
2
+ import { fileURLToPath } from "url";
3
+ import { dirname, join } from "path";
4
+
5
+ const _dir = dirname(fileURLToPath(import.meta.url));
6
+ export const VERSION: string = (
7
+ JSON.parse(readFileSync(join(_dir, "../package.json"), "utf8")) as { version: string }
8
+ ).version;
package/test/run-all.ts CHANGED
@@ -2,9 +2,21 @@
2
2
  * Integration test: calls each Buda MCP tool directly via BudaClient
3
3
  * and prints a summary of the results.
4
4
  *
5
- * Run with: npm test
5
+ * Run with: npm run test:integration
6
+ * Skipped automatically when the Buda API is unreachable (CI without network).
6
7
  */
7
8
 
9
+ // Connectivity pre-check — skip gracefully instead of failing CI when the API is unreachable.
10
+ try {
11
+ await fetch("https://www.buda.com/api/v2/markets.json", {
12
+ signal: AbortSignal.timeout(3000),
13
+ });
14
+ } catch {
15
+ console.log("\nBuda API is unreachable — skipping integration tests (no network or API down).");
16
+ console.log("Run unit tests only with: npm run test:unit");
17
+ process.exit(0);
18
+ }
19
+
8
20
  import { BudaClient } from "../src/client.js";
9
21
  import type {
10
22
  MarketsResponse,