@guiie/buda-mcp 1.0.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.
Files changed (44) hide show
  1. package/.github/workflows/publish.yml +58 -0
  2. package/PUBLISH.md +206 -0
  3. package/README.md +122 -0
  4. package/dist/client.d.ts +18 -0
  5. package/dist/client.d.ts.map +1 -0
  6. package/dist/client.js +52 -0
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +23 -0
  10. package/dist/tools/markets.d.ts +4 -0
  11. package/dist/tools/markets.d.ts.map +1 -0
  12. package/dist/tools/markets.js +22 -0
  13. package/dist/tools/orderbook.d.ts +4 -0
  14. package/dist/tools/orderbook.d.ts.map +1 -0
  15. package/dist/tools/orderbook.js +28 -0
  16. package/dist/tools/ticker.d.ts +4 -0
  17. package/dist/tools/ticker.d.ts.map +1 -0
  18. package/dist/tools/ticker.js +15 -0
  19. package/dist/tools/trades.d.ts +4 -0
  20. package/dist/tools/trades.d.ts.map +1 -0
  21. package/dist/tools/trades.js +32 -0
  22. package/dist/tools/volume.d.ts +4 -0
  23. package/dist/tools/volume.d.ts.map +1 -0
  24. package/dist/tools/volume.js +15 -0
  25. package/dist/types.d.ts +61 -0
  26. package/dist/types.d.ts.map +1 -0
  27. package/dist/types.js +4 -0
  28. package/marketplace/README.md +67 -0
  29. package/marketplace/claude-listing.md +97 -0
  30. package/marketplace/cursor-mcp.json +9 -0
  31. package/marketplace/gemini-tools.json +87 -0
  32. package/marketplace/openapi.yaml +349 -0
  33. package/package.json +53 -0
  34. package/server.json +20 -0
  35. package/src/client.ts +57 -0
  36. package/src/index.ts +26 -0
  37. package/src/tools/markets.ts +35 -0
  38. package/src/tools/orderbook.ts +40 -0
  39. package/src/tools/ticker.ts +25 -0
  40. package/src/tools/trades.ts +45 -0
  41. package/src/tools/volume.ts +25 -0
  42. package/src/types.ts +84 -0
  43. package/test/run-all.ts +144 -0
  44. package/tsconfig.json +17 -0
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@guiie/buda-mcp",
3
+ "version": "1.0.0",
4
+ "mcpName": "io.github.gtorreal/buda-mcp",
5
+ "description": "MCP server for Buda.com's public cryptocurrency exchange API (Chile, Colombia, Peru)",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "bin": {
9
+ "buda-mcp": "dist/index.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "start": "node dist/index.js",
14
+ "dev": "tsx src/index.ts",
15
+ "test": "tsx test/run-all.ts"
16
+ },
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "keywords": [
21
+ "mcp",
22
+ "buda",
23
+ "cryptocurrency",
24
+ "exchange",
25
+ "bitcoin",
26
+ "trading",
27
+ "chile",
28
+ "colombia",
29
+ "peru",
30
+ "clp",
31
+ "cop",
32
+ "pen"
33
+ ],
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/gtorreal/buda-mcp.git"
37
+ },
38
+ "homepage": "https://github.com/gtorreal/buda-mcp#readme",
39
+ "bugs": {
40
+ "url": "https://github.com/gtorreal/buda-mcp/issues"
41
+ },
42
+ "license": "MIT",
43
+ "author": "Guille <guillermo@buda.com>",
44
+ "dependencies": {
45
+ "@modelcontextprotocol/sdk": "^1.29.0",
46
+ "zod": "^4.3.6"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^25.6.0",
50
+ "tsx": "^4.21.0",
51
+ "typescript": "^6.0.2"
52
+ }
53
+ }
package/server.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.gtorreal/buda-mcp",
4
+ "description": "Real-time cryptocurrency market data from Buda.com — prices, order books, trades, and volume for markets in Chile (CLP), Colombia (COP), and Peru (PEN). No API key required.",
5
+ "repository": {
6
+ "url": "https://github.com/gtorreal/buda-mcp",
7
+ "source": "github"
8
+ },
9
+ "version": "1.0.0",
10
+ "packages": [
11
+ {
12
+ "registryType": "npm",
13
+ "identifier": "@guiie/buda-mcp",
14
+ "version": "1.0.0",
15
+ "transport": {
16
+ "type": "stdio"
17
+ }
18
+ }
19
+ ]
20
+ }
package/src/client.ts ADDED
@@ -0,0 +1,57 @@
1
+ const BASE_URL = "https://www.buda.com/api/v2";
2
+
3
+ export class BudaApiError extends Error {
4
+ constructor(
5
+ public readonly status: number,
6
+ public readonly path: string,
7
+ message: string,
8
+ ) {
9
+ super(message);
10
+ this.name = "BudaApiError";
11
+ }
12
+ }
13
+
14
+ export class BudaClient {
15
+ private readonly baseUrl: string;
16
+
17
+ constructor(baseUrl: string = BASE_URL) {
18
+ this.baseUrl = baseUrl;
19
+ }
20
+
21
+ /**
22
+ * Perform an authenticated GET request.
23
+ *
24
+ * Private endpoints (balances, orders, etc.) require HMAC-SHA2 signing.
25
+ * To add auth later, extend this method with the `apiKey` + `apiSecret`
26
+ * constructor params and sign the nonce/path headers here before fetching.
27
+ */
28
+ async get<T>(path: string, params?: Record<string, string | number>): Promise<T> {
29
+ const url = new URL(`${this.baseUrl}${path}.json`);
30
+
31
+ if (params) {
32
+ for (const [key, value] of Object.entries(params)) {
33
+ url.searchParams.set(key, String(value));
34
+ }
35
+ }
36
+
37
+ const response = await fetch(url.toString(), {
38
+ headers: {
39
+ Accept: "application/json",
40
+ "User-Agent": "buda-mcp/1.0.0",
41
+ },
42
+ });
43
+
44
+ if (!response.ok) {
45
+ let detail = response.statusText;
46
+ try {
47
+ const body = (await response.json()) as { message?: string };
48
+ if (body.message) detail = body.message;
49
+ } catch {
50
+ // ignore parse error, use statusText
51
+ }
52
+ throw new BudaApiError(response.status, path, `Buda API ${response.status}: ${detail}`);
53
+ }
54
+
55
+ return response.json() as Promise<T>;
56
+ }
57
+ }
package/src/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { BudaClient } from "./client.js";
4
+ import * as markets from "./tools/markets.js";
5
+ import * as ticker from "./tools/ticker.js";
6
+ import * as orderbook from "./tools/orderbook.js";
7
+ import * as trades from "./tools/trades.js";
8
+ import * as volume from "./tools/volume.js";
9
+
10
+ const server = new McpServer({
11
+ name: "buda-mcp",
12
+ version: "1.0.0",
13
+ });
14
+
15
+ const client = new BudaClient();
16
+
17
+ // Register all public-endpoint tools
18
+ markets.register(server, client);
19
+ ticker.register(server, client);
20
+ orderbook.register(server, client);
21
+ trades.register(server, client);
22
+ volume.register(server, client);
23
+
24
+ // Start the server over stdio (standard MCP transport)
25
+ const transport = new StdioServerTransport();
26
+ await server.connect(transport);
@@ -0,0 +1,35 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { BudaClient } from "../client.js";
4
+ import type { MarketsResponse, MarketResponse } from "../types.js";
5
+
6
+ export function register(server: McpServer, client: BudaClient): void {
7
+ server.tool(
8
+ "get_markets",
9
+ "List all available trading pairs on Buda.com, or get details for a specific market. " +
10
+ "Returns base/quote currencies, fees, and minimum order sizes.",
11
+ {
12
+ market_id: z
13
+ .string()
14
+ .optional()
15
+ .describe(
16
+ "Optional market ID (e.g. 'BTC-CLP', 'ETH-BTC'). Omit to list all markets.",
17
+ ),
18
+ },
19
+ async ({ market_id }) => {
20
+ if (market_id) {
21
+ const data = await client.get<MarketResponse>(
22
+ `/markets/${market_id.toLowerCase()}`,
23
+ );
24
+ return {
25
+ content: [{ type: "text", text: JSON.stringify(data.market, null, 2) }],
26
+ };
27
+ }
28
+
29
+ const data = await client.get<MarketsResponse>("/markets");
30
+ return {
31
+ content: [{ type: "text", text: JSON.stringify(data.markets, null, 2) }],
32
+ };
33
+ },
34
+ );
35
+ }
@@ -0,0 +1,40 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { BudaClient } from "../client.js";
4
+ import type { OrderBookResponse } from "../types.js";
5
+
6
+ export function register(server: McpServer, client: BudaClient): void {
7
+ server.tool(
8
+ "get_orderbook",
9
+ "Get the current order book for a Buda.com market. Returns sorted arrays of " +
10
+ "bids (buy orders) and asks (sell orders), each as [price, amount] pairs.",
11
+ {
12
+ market_id: z
13
+ .string()
14
+ .describe("Market ID (e.g. 'BTC-CLP', 'ETH-BTC')."),
15
+ limit: z
16
+ .number()
17
+ .int()
18
+ .positive()
19
+ .optional()
20
+ .describe("Maximum number of levels to return per side (default: all)."),
21
+ },
22
+ async ({ market_id, limit }) => {
23
+ const data = await client.get<OrderBookResponse>(
24
+ `/markets/${market_id.toLowerCase()}/order_book`,
25
+ );
26
+
27
+ const book = data.order_book;
28
+ const result = {
29
+ bids: limit ? book.bids.slice(0, limit) : book.bids,
30
+ asks: limit ? book.asks.slice(0, limit) : book.asks,
31
+ bid_count: book.bids.length,
32
+ ask_count: book.asks.length,
33
+ };
34
+
35
+ return {
36
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
37
+ };
38
+ },
39
+ );
40
+ }
@@ -0,0 +1,25 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { BudaClient } from "../client.js";
4
+ import type { TickerResponse } from "../types.js";
5
+
6
+ export function register(server: McpServer, client: BudaClient): void {
7
+ server.tool(
8
+ "get_ticker",
9
+ "Get the current ticker for a Buda.com market: last traded price, best bid/ask, " +
10
+ "24h volume, and price change over 24h and 7d.",
11
+ {
12
+ market_id: z
13
+ .string()
14
+ .describe("Market ID (e.g. 'BTC-CLP', 'ETH-BTC', 'BTC-COP')."),
15
+ },
16
+ async ({ market_id }) => {
17
+ const data = await client.get<TickerResponse>(
18
+ `/markets/${market_id.toLowerCase()}/ticker`,
19
+ );
20
+ return {
21
+ content: [{ type: "text", text: JSON.stringify(data.ticker, null, 2) }],
22
+ };
23
+ },
24
+ );
25
+ }
@@ -0,0 +1,45 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { BudaClient } from "../client.js";
4
+ import type { TradesResponse } from "../types.js";
5
+
6
+ export function register(server: McpServer, client: BudaClient): void {
7
+ server.tool(
8
+ "get_trades",
9
+ "Get recent trade history for a Buda.com market. Each entry contains " +
10
+ "[timestamp_ms, amount, price, direction]. Direction is 'buy' or 'sell'.",
11
+ {
12
+ market_id: z
13
+ .string()
14
+ .describe("Market ID (e.g. 'BTC-CLP', 'ETH-BTC')."),
15
+ limit: z
16
+ .number()
17
+ .int()
18
+ .min(1)
19
+ .max(100)
20
+ .optional()
21
+ .describe("Number of trades to return (default: 50, max: 100)."),
22
+ timestamp: z
23
+ .number()
24
+ .int()
25
+ .optional()
26
+ .describe(
27
+ "Unix timestamp (seconds) to paginate from. Returns trades older than this timestamp.",
28
+ ),
29
+ },
30
+ async ({ market_id, limit, timestamp }) => {
31
+ const params: Record<string, string | number> = {};
32
+ if (limit !== undefined) params.limit = limit;
33
+ if (timestamp !== undefined) params.timestamp = timestamp;
34
+
35
+ const data = await client.get<TradesResponse>(
36
+ `/markets/${market_id.toLowerCase()}/trades`,
37
+ Object.keys(params).length > 0 ? params : undefined,
38
+ );
39
+
40
+ return {
41
+ content: [{ type: "text", text: JSON.stringify(data.trades, null, 2) }],
42
+ };
43
+ },
44
+ );
45
+ }
@@ -0,0 +1,25 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { BudaClient } from "../client.js";
4
+ import type { VolumeResponse } from "../types.js";
5
+
6
+ export function register(server: McpServer, client: BudaClient): void {
7
+ server.tool(
8
+ "get_market_volume",
9
+ "Get 24h and 7-day transacted volume for a Buda.com market. " +
10
+ "Returns ask (sell) and bid (buy) volumes in the market's base currency.",
11
+ {
12
+ market_id: z
13
+ .string()
14
+ .describe("Market ID (e.g. 'BTC-CLP', 'ETH-BTC')."),
15
+ },
16
+ async ({ market_id }) => {
17
+ const data = await client.get<VolumeResponse>(
18
+ `/markets/${market_id.toLowerCase()}/volume`,
19
+ );
20
+ return {
21
+ content: [{ type: "text", text: JSON.stringify(data.volume, null, 2) }],
22
+ };
23
+ },
24
+ );
25
+ }
package/src/types.ts ADDED
@@ -0,0 +1,84 @@
1
+ // Buda.com REST API v2 — shared response types
2
+ // All monetary amounts are returned as [amount_string, currency_string] tuples.
3
+
4
+ export type Amount = [string, string];
5
+
6
+ // ----- Markets -----
7
+
8
+ export interface Market {
9
+ id: string;
10
+ name: string;
11
+ base_currency: string;
12
+ quote_currency: string;
13
+ minimum_order_amount: Amount;
14
+ taker_fee: string;
15
+ maker_fee: string;
16
+ max_orders_per_minute: number;
17
+ maker_discount_percentage: string;
18
+ taker_discount_percentage: string;
19
+ maker_discount_tiers: Record<string, number>;
20
+ taker_discount_tiers: Record<string, number>;
21
+ }
22
+
23
+ export interface MarketsResponse {
24
+ markets: Market[];
25
+ }
26
+
27
+ export interface MarketResponse {
28
+ market: Market;
29
+ }
30
+
31
+ // ----- Ticker -----
32
+
33
+ export interface Ticker {
34
+ market_id: string;
35
+ last_price: Amount;
36
+ min_ask: Amount;
37
+ max_bid: Amount;
38
+ volume: Amount;
39
+ price_variation_24h: string;
40
+ price_variation_7d: string;
41
+ }
42
+
43
+ export interface TickerResponse {
44
+ ticker: Ticker;
45
+ }
46
+
47
+ // ----- Order Book -----
48
+
49
+ export interface OrderBook {
50
+ asks: [string, string][];
51
+ bids: [string, string][];
52
+ }
53
+
54
+ export interface OrderBookResponse {
55
+ order_book: OrderBook;
56
+ }
57
+
58
+ // ----- Trades -----
59
+
60
+ export interface Trades {
61
+ timestamp: string;
62
+ last_timestamp: string;
63
+ market_id: string;
64
+ /** Each entry: [timestamp, amount, price, direction] */
65
+ entries: [string, string, string, string][];
66
+ }
67
+
68
+ export interface TradesResponse {
69
+ trades: Trades;
70
+ }
71
+
72
+ // ----- Volume -----
73
+
74
+ export interface MarketVolume {
75
+ market_id: string;
76
+ ask_volume_24h: Amount;
77
+ ask_volume_7d: Amount;
78
+ bid_volume_24h: Amount;
79
+ bid_volume_7d: Amount;
80
+ }
81
+
82
+ export interface VolumeResponse {
83
+ volume: MarketVolume;
84
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Integration test: calls each Buda MCP tool directly via BudaClient
3
+ * and prints a summary of the results.
4
+ *
5
+ * Run with: npm test
6
+ */
7
+
8
+ import { BudaClient } from "../src/client.js";
9
+ import type {
10
+ MarketsResponse,
11
+ TickerResponse,
12
+ OrderBookResponse,
13
+ TradesResponse,
14
+ VolumeResponse,
15
+ } from "../src/types.js";
16
+
17
+ const client = new BudaClient();
18
+ const TEST_MARKET = "BTC-CLP";
19
+
20
+ function section(title: string): void {
21
+ console.log("\n" + "=".repeat(60));
22
+ console.log(` ${title}`);
23
+ console.log("=".repeat(60));
24
+ }
25
+
26
+ function pass(label: string, detail: string): void {
27
+ console.log(` PASS ${label}: ${detail}`);
28
+ }
29
+
30
+ function fail(label: string, error: unknown): void {
31
+ console.error(` FAIL ${label}:`, error instanceof Error ? error.message : error);
32
+ }
33
+
34
+ let failures = 0;
35
+
36
+ // ----------------------------------------------------------------
37
+ // 1. get_markets
38
+ // ----------------------------------------------------------------
39
+ section("get_markets — list all markets");
40
+ try {
41
+ const data = await client.get<MarketsResponse>("/markets");
42
+ const ids = data.markets.map((m) => m.id);
43
+ pass("markets count", `${ids.length} markets returned`);
44
+ pass("includes BTC-CLP", String(ids.includes("BTC-CLP")));
45
+ console.log(" Sample IDs:", ids.slice(0, 6).join(", "));
46
+ } catch (err) {
47
+ fail("get_markets", err);
48
+ failures++;
49
+ }
50
+
51
+ // ----------------------------------------------------------------
52
+ // 2. get_ticker
53
+ // ----------------------------------------------------------------
54
+ section(`get_ticker — ${TEST_MARKET}`);
55
+ try {
56
+ const data = await client.get<TickerResponse>(`/markets/${TEST_MARKET.toLowerCase()}/ticker`);
57
+ const t = data.ticker;
58
+ pass("market_id", t.market_id);
59
+ pass("last_price", `${t.last_price[0]} ${t.last_price[1]}`);
60
+ pass("max_bid", `${t.max_bid[0]} ${t.max_bid[1]}`);
61
+ pass("min_ask", `${t.min_ask[0]} ${t.min_ask[1]}`);
62
+ pass("volume_24h", `${t.volume[0]} ${t.volume[1]}`);
63
+ pass("price_variation_24h", `${(parseFloat(t.price_variation_24h) * 100).toFixed(2)}%`);
64
+ } catch (err) {
65
+ fail("get_ticker", err);
66
+ failures++;
67
+ }
68
+
69
+ // ----------------------------------------------------------------
70
+ // 3. get_orderbook
71
+ // ----------------------------------------------------------------
72
+ section(`get_orderbook — ${TEST_MARKET}`);
73
+ try {
74
+ const data = await client.get<OrderBookResponse>(
75
+ `/markets/${TEST_MARKET.toLowerCase()}/order_book`,
76
+ );
77
+ const book = data.order_book;
78
+ pass("bids count", `${book.bids.length} levels`);
79
+ pass("asks count", `${book.asks.length} levels`);
80
+ if (book.bids.length > 0 && book.asks.length > 0) {
81
+ pass("top bid", `${book.bids[0][0]} @ ${book.bids[0][1]} BTC`);
82
+ pass("top ask", `${book.asks[0][0]} @ ${book.asks[0][1]} BTC`);
83
+ const spread =
84
+ parseFloat(book.asks[0][0]) - parseFloat(book.bids[0][0]);
85
+ pass("spread", spread.toFixed(2));
86
+ }
87
+ } catch (err) {
88
+ fail("get_orderbook", err);
89
+ failures++;
90
+ }
91
+
92
+ // ----------------------------------------------------------------
93
+ // 4. get_trades
94
+ // ----------------------------------------------------------------
95
+ section(`get_trades — ${TEST_MARKET} (limit 10)`);
96
+ try {
97
+ const data = await client.get<TradesResponse>(
98
+ `/markets/${TEST_MARKET.toLowerCase()}/trades`,
99
+ { limit: 10 },
100
+ );
101
+ const t = data.trades;
102
+ pass("market_id", t.market_id);
103
+ pass("entries count", `${t.entries.length}`);
104
+ if (t.entries.length > 0) {
105
+ const [ts, amount, price, direction] = t.entries[0];
106
+ pass(
107
+ "latest trade",
108
+ `${direction} ${amount} BTC @ ${price} CLP (ts: ${ts})`,
109
+ );
110
+ }
111
+ } catch (err) {
112
+ fail("get_trades", err);
113
+ failures++;
114
+ }
115
+
116
+ // ----------------------------------------------------------------
117
+ // 5. get_market_volume
118
+ // ----------------------------------------------------------------
119
+ section(`get_market_volume — ${TEST_MARKET}`);
120
+ try {
121
+ const data = await client.get<VolumeResponse>(
122
+ `/markets/${TEST_MARKET.toLowerCase()}/volume`,
123
+ );
124
+ const v = data.volume;
125
+ pass("market_id", v.market_id);
126
+ pass("ask_volume_24h", `${v.ask_volume_24h[0]} ${v.ask_volume_24h[1]}`);
127
+ pass("ask_volume_7d", `${v.ask_volume_7d[0]} ${v.ask_volume_7d[1]}`);
128
+ pass("bid_volume_24h", `${v.bid_volume_24h[0]} ${v.bid_volume_24h[1]}`);
129
+ pass("bid_volume_7d", `${v.bid_volume_7d[0]} ${v.bid_volume_7d[1]}`);
130
+ } catch (err) {
131
+ fail("get_market_volume", err);
132
+ failures++;
133
+ }
134
+
135
+ // ----------------------------------------------------------------
136
+ // Summary
137
+ // ----------------------------------------------------------------
138
+ section("Summary");
139
+ if (failures === 0) {
140
+ console.log(" All tools returned valid data from the live Buda API.");
141
+ } else {
142
+ console.error(` ${failures} tool(s) failed. See errors above.`);
143
+ process.exit(1);
144
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "declaration": true,
12
+ "declarationMap": true,
13
+ "sourceMap": true
14
+ },
15
+ "include": ["src"],
16
+ "exclude": ["node_modules", "dist", "test"]
17
+ }