@guiie/buda-mcp 1.2.2 → 1.4.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 (79) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/PUBLISH_CHECKLIST.md +71 -63
  3. package/README.md +4 -4
  4. package/dist/http.js +39 -0
  5. package/dist/index.js +29 -0
  6. package/dist/tools/arbitrage.d.ts +35 -0
  7. package/dist/tools/arbitrage.d.ts.map +1 -0
  8. package/dist/tools/arbitrage.js +142 -0
  9. package/dist/tools/balances.d.ts.map +1 -1
  10. package/dist/tools/balances.js +24 -4
  11. package/dist/tools/calculate_position_size.d.ts +48 -0
  12. package/dist/tools/calculate_position_size.d.ts.map +1 -0
  13. package/dist/tools/calculate_position_size.js +111 -0
  14. package/dist/tools/compare_markets.d.ts.map +1 -1
  15. package/dist/tools/compare_markets.js +11 -10
  16. package/dist/tools/dead_mans_switch.d.ts +84 -0
  17. package/dist/tools/dead_mans_switch.d.ts.map +1 -0
  18. package/dist/tools/dead_mans_switch.js +236 -0
  19. package/dist/tools/market_sentiment.d.ts +30 -0
  20. package/dist/tools/market_sentiment.d.ts.map +1 -0
  21. package/dist/tools/market_sentiment.js +104 -0
  22. package/dist/tools/market_summary.d.ts +43 -0
  23. package/dist/tools/market_summary.d.ts.map +1 -0
  24. package/dist/tools/market_summary.js +81 -0
  25. package/dist/tools/markets.d.ts.map +1 -1
  26. package/dist/tools/markets.js +4 -2
  27. package/dist/tools/orderbook.d.ts.map +1 -1
  28. package/dist/tools/orderbook.js +14 -4
  29. package/dist/tools/orders.d.ts.map +1 -1
  30. package/dist/tools/orders.js +41 -3
  31. package/dist/tools/price_history.d.ts.map +1 -1
  32. package/dist/tools/price_history.js +5 -43
  33. package/dist/tools/simulate_order.d.ts +45 -0
  34. package/dist/tools/simulate_order.d.ts.map +1 -0
  35. package/dist/tools/simulate_order.js +139 -0
  36. package/dist/tools/spread.d.ts.map +1 -1
  37. package/dist/tools/spread.js +10 -8
  38. package/dist/tools/technical_indicators.d.ts +39 -0
  39. package/dist/tools/technical_indicators.d.ts.map +1 -0
  40. package/dist/tools/technical_indicators.js +223 -0
  41. package/dist/tools/ticker.d.ts.map +1 -1
  42. package/dist/tools/ticker.js +24 -3
  43. package/dist/tools/trades.d.ts.map +1 -1
  44. package/dist/tools/trades.js +17 -3
  45. package/dist/tools/volume.d.ts.map +1 -1
  46. package/dist/tools/volume.js +21 -3
  47. package/dist/types.d.ts +9 -0
  48. package/dist/types.d.ts.map +1 -1
  49. package/dist/utils.d.ts +23 -0
  50. package/dist/utils.d.ts.map +1 -0
  51. package/dist/utils.js +68 -0
  52. package/marketplace/README.md +1 -1
  53. package/marketplace/claude-listing.md +60 -14
  54. package/marketplace/gemini-tools.json +183 -9
  55. package/marketplace/openapi.yaml +335 -119
  56. package/package.json +1 -1
  57. package/server.json +2 -2
  58. package/src/http.ts +44 -0
  59. package/src/index.ts +34 -0
  60. package/src/tools/arbitrage.ts +202 -0
  61. package/src/tools/balances.ts +27 -4
  62. package/src/tools/calculate_position_size.ts +141 -0
  63. package/src/tools/compare_markets.ts +11 -10
  64. package/src/tools/dead_mans_switch.ts +314 -0
  65. package/src/tools/market_sentiment.ts +141 -0
  66. package/src/tools/market_summary.ts +124 -0
  67. package/src/tools/markets.ts +4 -2
  68. package/src/tools/orderbook.ts +15 -4
  69. package/src/tools/orders.ts +45 -4
  70. package/src/tools/price_history.ts +5 -57
  71. package/src/tools/simulate_order.ts +182 -0
  72. package/src/tools/spread.ts +10 -8
  73. package/src/tools/technical_indicators.ts +282 -0
  74. package/src/tools/ticker.ts +27 -3
  75. package/src/tools/trades.ts +18 -3
  76. package/src/tools/volume.ts +24 -3
  77. package/src/types.ts +12 -0
  78. package/src/utils.ts +73 -0
  79. package/test/unit.ts +758 -0
@@ -1,9 +1,12 @@
1
1
  import { BudaApiError } from "../client.js";
2
+ import { flattenAmount } from "../utils.js";
2
3
  export const toolSchema = {
3
4
  name: "get_balances",
4
- description: "Get all currency balances for the authenticated Buda.com account. " +
5
- "Returns total, available, frozen, and pending withdrawal amounts per currency. " +
6
- "Requires BUDA_API_KEY and BUDA_API_SECRET environment variables.",
5
+ description: "Returns all currency balances for the authenticated Buda.com account as flat typed objects. " +
6
+ "Each currency entry includes total amount, available amount (not frozen), frozen amount, and " +
7
+ "pending withdrawal amount all as floats with separate _currency fields. " +
8
+ "Requires BUDA_API_KEY and BUDA_API_SECRET. " +
9
+ "Example: 'How much BTC do I have available to trade right now?'",
7
10
  inputSchema: {
8
11
  type: "object",
9
12
  properties: {},
@@ -13,8 +16,25 @@ export function register(server, client) {
13
16
  server.tool(toolSchema.name, toolSchema.description, {}, async () => {
14
17
  try {
15
18
  const data = await client.get("/balances");
19
+ const result = data.balances.map((b) => {
20
+ const amount = flattenAmount(b.amount);
21
+ const available = flattenAmount(b.available_amount);
22
+ const frozen = flattenAmount(b.frozen_amount);
23
+ const pending = flattenAmount(b.pending_withdraw_amount);
24
+ return {
25
+ id: b.id,
26
+ amount: amount.value,
27
+ amount_currency: amount.currency,
28
+ available_amount: available.value,
29
+ available_amount_currency: available.currency,
30
+ frozen_amount: frozen.value,
31
+ frozen_amount_currency: frozen.currency,
32
+ pending_withdraw_amount: pending.value,
33
+ pending_withdraw_amount_currency: pending.currency,
34
+ };
35
+ });
16
36
  return {
17
- content: [{ type: "text", text: JSON.stringify(data.balances, null, 2) }],
37
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
18
38
  };
19
39
  }
20
40
  catch (err) {
@@ -0,0 +1,48 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare const toolSchema: {
3
+ name: string;
4
+ description: string;
5
+ inputSchema: {
6
+ type: "object";
7
+ properties: {
8
+ market_id: {
9
+ type: string;
10
+ description: string;
11
+ };
12
+ capital: {
13
+ type: string;
14
+ description: string;
15
+ };
16
+ risk_pct: {
17
+ type: string;
18
+ description: string;
19
+ };
20
+ entry_price: {
21
+ type: string;
22
+ description: string;
23
+ };
24
+ stop_loss_price: {
25
+ type: string;
26
+ description: string;
27
+ };
28
+ };
29
+ required: string[];
30
+ };
31
+ };
32
+ type CalculatePositionSizeArgs = {
33
+ market_id: string;
34
+ capital: number;
35
+ risk_pct: number;
36
+ entry_price: number;
37
+ stop_loss_price: number;
38
+ };
39
+ export declare function handleCalculatePositionSize(args: CalculatePositionSizeArgs): {
40
+ content: Array<{
41
+ type: "text";
42
+ text: string;
43
+ }>;
44
+ isError?: boolean;
45
+ };
46
+ export declare function register(server: McpServer): void;
47
+ export {};
48
+ //# sourceMappingURL=calculate_position_size.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calculate_position_size.d.ts","sourceRoot":"","sources":["../../src/tools/calculate_position_size.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIpE,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCtB,CAAC;AAEF,KAAK,yBAAyB,GAAG;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,yBAAyB,GAC9B;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CA0DvE;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA8BhD"}
@@ -0,0 +1,111 @@
1
+ import { z } from "zod";
2
+ import { validateMarketId } from "../validation.js";
3
+ export const toolSchema = {
4
+ name: "calculate_position_size",
5
+ description: "Calculates position size based on your capital, risk tolerance, entry price, and stop-loss. " +
6
+ "Determines how many units to buy or sell so that a stop-loss hit costs exactly risk_pct% of capital. " +
7
+ "Fully client-side — no API call is made. " +
8
+ "Example: 'How many BTC can I buy on BTC-CLP if I have 1,000,000 CLP, risk 2%, entry 80,000,000 CLP, stop at 78,000,000 CLP?'",
9
+ inputSchema: {
10
+ type: "object",
11
+ properties: {
12
+ market_id: {
13
+ type: "string",
14
+ description: "Market ID (e.g. 'BTC-CLP', 'ETH-COP'). Used to derive the quote currency.",
15
+ },
16
+ capital: {
17
+ type: "number",
18
+ description: "Total available capital in the quote currency (e.g. CLP for BTC-CLP).",
19
+ },
20
+ risk_pct: {
21
+ type: "number",
22
+ description: "Percentage of capital to risk on this trade (0.1–10, e.g. 2 = 2%).",
23
+ },
24
+ entry_price: {
25
+ type: "number",
26
+ description: "Planned entry price in quote currency.",
27
+ },
28
+ stop_loss_price: {
29
+ type: "number",
30
+ description: "Stop-loss price in quote currency. Must be below entry for buys, above entry for sells.",
31
+ },
32
+ },
33
+ required: ["market_id", "capital", "risk_pct", "entry_price", "stop_loss_price"],
34
+ },
35
+ };
36
+ export function handleCalculatePositionSize(args) {
37
+ const { market_id, capital, risk_pct, entry_price, stop_loss_price } = args;
38
+ const validationError = validateMarketId(market_id);
39
+ if (validationError) {
40
+ return {
41
+ content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_MARKET_ID" }) }],
42
+ isError: true,
43
+ };
44
+ }
45
+ if (stop_loss_price === entry_price) {
46
+ return {
47
+ content: [
48
+ {
49
+ type: "text",
50
+ text: JSON.stringify({
51
+ error: "stop_loss_price must differ from entry_price.",
52
+ code: "INVALID_STOP_LOSS",
53
+ }),
54
+ },
55
+ ],
56
+ isError: true,
57
+ };
58
+ }
59
+ const quoteCurrency = market_id.split("-")[1].toUpperCase();
60
+ const baseCurrency = market_id.split("-")[0].toUpperCase();
61
+ const side = stop_loss_price < entry_price ? "buy" : "sell";
62
+ const capitalAtRisk = capital * (risk_pct / 100);
63
+ const riskPerUnit = Math.abs(entry_price - stop_loss_price);
64
+ const units = capitalAtRisk / riskPerUnit;
65
+ const positionValue = units * entry_price;
66
+ const feeImpact = parseFloat((positionValue * 0.008).toFixed(8));
67
+ const riskRewardNote = `${side === "buy" ? "Buy" : "Sell"} ${units.toFixed(8)} ${baseCurrency} at ${entry_price} ${quoteCurrency} ` +
68
+ `with stop at ${stop_loss_price} ${quoteCurrency}. ` +
69
+ `Risking ${risk_pct}% of capital (${capitalAtRisk.toFixed(2)} ${quoteCurrency}) ` +
70
+ `on a ${riskPerUnit.toFixed(2)} ${quoteCurrency}/unit move. ` +
71
+ `Estimated entry fee: ${feeImpact.toFixed(2)} ${quoteCurrency} (0.8% taker, conservative estimate).`;
72
+ const result = {
73
+ market_id: market_id.toUpperCase(),
74
+ side,
75
+ units: parseFloat(units.toFixed(8)),
76
+ base_currency: baseCurrency,
77
+ capital_at_risk: parseFloat(capitalAtRisk.toFixed(2)),
78
+ position_value: parseFloat(positionValue.toFixed(2)),
79
+ fee_impact: feeImpact,
80
+ fee_currency: quoteCurrency,
81
+ risk_reward_note: riskRewardNote,
82
+ };
83
+ return {
84
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
85
+ };
86
+ }
87
+ export function register(server) {
88
+ server.tool(toolSchema.name, toolSchema.description, {
89
+ market_id: z
90
+ .string()
91
+ .describe("Market ID (e.g. 'BTC-CLP', 'ETH-COP'). Used to derive the quote currency."),
92
+ capital: z
93
+ .number()
94
+ .positive()
95
+ .describe("Total available capital in the quote currency (e.g. CLP for BTC-CLP)."),
96
+ risk_pct: z
97
+ .number()
98
+ .min(0.1)
99
+ .max(10)
100
+ .describe("Percentage of capital to risk on this trade (0.1–10, e.g. 2 = 2%)."),
101
+ entry_price: z
102
+ .number()
103
+ .positive()
104
+ .describe("Planned entry price in quote currency."),
105
+ stop_loss_price: z
106
+ .number()
107
+ .positive()
108
+ .describe("Stop-loss price in quote currency. Must be below entry for buys, above entry for sells."),
109
+ }, (args) => handleCalculatePositionSize(args));
110
+ }
111
+ //# sourceMappingURL=calculate_position_size.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"compare_markets.d.ts","sourceRoot":"","sources":["../../src/tools/compare_markets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,UAAU,EAAgB,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,WAAW,EAAa,MAAM,aAAa,CAAC;AAGrD,eAAO,MAAM,UAAU;;;;;;;;;;;;;CAiBtB,CAAC;AAEF,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAyExF"}
1
+ {"version":3,"file":"compare_markets.d.ts","sourceRoot":"","sources":["../../src/tools/compare_markets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,UAAU,EAAgB,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,WAAW,EAAa,MAAM,aAAa,CAAC;AAGrD,eAAO,MAAM,UAAU;;;;;;;;;;;;;CAkBtB,CAAC;AAEF,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAyExF"}
@@ -3,9 +3,10 @@ import { BudaApiError } from "../client.js";
3
3
  import { CACHE_TTL } from "../cache.js";
4
4
  export const toolSchema = {
5
5
  name: "compare_markets",
6
- description: "Compare ticker data for all trading pairs of a given base currency across Buda.com's " +
7
- "supported quote currencies (CLP, COP, PEN, BTC, USDC, ETH). " +
8
- "For example, passing 'BTC' returns side-by-side data for BTC-CLP, BTC-COP, BTC-PEN, etc.",
6
+ description: "Returns side-by-side ticker data for all trading pairs of a given base currency across Buda.com's " +
7
+ "supported quote currencies (CLP, COP, PEN, BTC, USDC, ETH). All prices are floats; " +
8
+ "price_change_24h and price_change_7d are floats in percent (e.g. 1.23 means +1.23%). " +
9
+ "Example: 'In which country is Bitcoin currently most expensive on Buda?'",
9
10
  inputSchema: {
10
11
  type: "object",
11
12
  properties: {
@@ -48,16 +49,16 @@ export function register(server, client, cache) {
48
49
  base_currency: base,
49
50
  markets: matching.map((t) => ({
50
51
  market_id: t.market_id,
51
- last_price: t.last_price[0],
52
- currency: t.last_price[1],
53
- best_bid: t.max_bid ? t.max_bid[0] : null,
54
- best_ask: t.min_ask ? t.min_ask[0] : null,
55
- volume_24h: t.volume ? t.volume[0] : null,
52
+ last_price: parseFloat(t.last_price[0]),
53
+ last_price_currency: t.last_price[1],
54
+ best_bid: t.max_bid ? parseFloat(t.max_bid[0]) : null,
55
+ best_ask: t.min_ask ? parseFloat(t.min_ask[0]) : null,
56
+ volume_24h: t.volume ? parseFloat(t.volume[0]) : null,
56
57
  price_change_24h: t.price_variation_24h
57
- ? (parseFloat(t.price_variation_24h) * 100).toFixed(2) + "%"
58
+ ? parseFloat((parseFloat(t.price_variation_24h) * 100).toFixed(4))
58
59
  : null,
59
60
  price_change_7d: t.price_variation_7d
60
- ? (parseFloat(t.price_variation_7d) * 100).toFixed(2) + "%"
61
+ ? parseFloat((parseFloat(t.price_variation_7d) * 100).toFixed(4))
61
62
  : null,
62
63
  })),
63
64
  };
@@ -0,0 +1,84 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { BudaClient } from "../client.js";
3
+ export declare const toolSchema: {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: {
7
+ type: "object";
8
+ properties: {
9
+ market_id: {
10
+ type: string;
11
+ description: string;
12
+ };
13
+ ttl_seconds: {
14
+ type: string;
15
+ description: string;
16
+ };
17
+ confirmation_token: {
18
+ type: string;
19
+ description: string;
20
+ };
21
+ };
22
+ required: string[];
23
+ };
24
+ };
25
+ export declare const renewToolSchema: {
26
+ name: string;
27
+ description: string;
28
+ inputSchema: {
29
+ type: "object";
30
+ properties: {
31
+ market_id: {
32
+ type: string;
33
+ description: string;
34
+ };
35
+ };
36
+ required: string[];
37
+ };
38
+ };
39
+ export declare const disarmToolSchema: {
40
+ name: string;
41
+ description: string;
42
+ inputSchema: {
43
+ type: "object";
44
+ properties: {
45
+ market_id: {
46
+ type: string;
47
+ description: string;
48
+ };
49
+ };
50
+ required: string[];
51
+ };
52
+ };
53
+ type ScheduleArgs = {
54
+ market_id: string;
55
+ ttl_seconds: number;
56
+ confirmation_token: string;
57
+ };
58
+ export declare function handleScheduleCancelAll(args: ScheduleArgs, client: BudaClient): Promise<{
59
+ content: Array<{
60
+ type: "text";
61
+ text: string;
62
+ }>;
63
+ isError?: boolean;
64
+ }>;
65
+ type MarketOnlyArgs = {
66
+ market_id: string;
67
+ };
68
+ export declare function handleRenewCancelTimer({ market_id }: MarketOnlyArgs, client: BudaClient): {
69
+ content: Array<{
70
+ type: "text";
71
+ text: string;
72
+ }>;
73
+ isError?: boolean;
74
+ };
75
+ export declare function handleDisarmCancelTimer({ market_id }: MarketOnlyArgs): {
76
+ content: Array<{
77
+ type: "text";
78
+ text: string;
79
+ }>;
80
+ isError?: boolean;
81
+ };
82
+ export declare function register(server: McpServer, client: BudaClient): void;
83
+ export {};
84
+ //# sourceMappingURL=dead_mans_switch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dead_mans_switch.d.ts","sourceRoot":"","sources":["../../src/tools/dead_mans_switch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,UAAU,EAAgB,MAAM,cAAc,CAAC;AAiDxD,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;CA2BtB,CAAC;AAEF,eAAO,MAAM,eAAe;;;;;;;;;;;;;CAgB3B,CAAC;AAEF,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;CAgB5B,CAAC;AAIF,KAAK,YAAY,GAAG;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CA+ChF;AAED,KAAK,cAAc,GAAG;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5C,wBAAgB,sBAAsB,CACpC,EAAE,SAAS,EAAE,EAAE,cAAc,EAC7B,MAAM,EAAE,UAAU,GACjB;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CA0CvE;AAED,wBAAgB,uBAAuB,CACrC,EAAE,SAAS,EAAE,EAAE,cAAc,GAC5B;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAwCvE;AAID,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CA0CpE"}
@@ -0,0 +1,236 @@
1
+ import { z } from "zod";
2
+ import { validateMarketId } from "../validation.js";
3
+ const timers = new Map();
4
+ async function cancelAllOrdersForMarket(marketId, client) {
5
+ try {
6
+ const data = await client.get(`/markets/${marketId}/orders`, { state: "pending", per: 300 });
7
+ const orders = data.orders ?? [];
8
+ await Promise.allSettled(orders.map((order) => client.put(`/orders/${order.id}`, { state: "canceling" })));
9
+ timers.delete(marketId);
10
+ }
11
+ catch {
12
+ // Swallow errors — the timer has fired; we cannot surface them to the caller
13
+ timers.delete(marketId);
14
+ }
15
+ }
16
+ function armTimer(marketId, ttlSeconds, client) {
17
+ const existing = timers.get(marketId);
18
+ if (existing)
19
+ clearTimeout(existing.timeout);
20
+ const expiresAt = Date.now() + ttlSeconds * 1000;
21
+ const timeout = setTimeout(() => {
22
+ void cancelAllOrdersForMarket(marketId, client);
23
+ }, ttlSeconds * 1000);
24
+ const entry = { timeout, expiresAt, ttlSeconds };
25
+ timers.set(marketId, entry);
26
+ return entry;
27
+ }
28
+ // ---- Tool schemas ----
29
+ export const toolSchema = {
30
+ name: "schedule_cancel_all",
31
+ description: "WARNING: timer state is lost on server restart. Not suitable as a production dead man's switch " +
32
+ "on hosted deployments (e.g. Railway). Use only on locally-run instances.\n\n" +
33
+ "Arms an in-memory dead man's switch: if not renewed within ttl_seconds, all open orders for the " +
34
+ "market are automatically cancelled. Requires confirmation_token='CONFIRM' to activate. " +
35
+ "Use renew_cancel_timer to reset the countdown, or disarm_cancel_timer to cancel without touching orders. " +
36
+ "Requires BUDA_API_KEY and BUDA_API_SECRET environment variables.",
37
+ inputSchema: {
38
+ type: "object",
39
+ properties: {
40
+ market_id: {
41
+ type: "string",
42
+ description: "Market ID to protect (e.g. 'BTC-CLP').",
43
+ },
44
+ ttl_seconds: {
45
+ type: "number",
46
+ description: "Seconds before all orders are cancelled if not renewed (10–300).",
47
+ },
48
+ confirmation_token: {
49
+ type: "string",
50
+ description: "Must equal exactly 'CONFIRM' (case-sensitive) to arm the switch.",
51
+ },
52
+ },
53
+ required: ["market_id", "ttl_seconds", "confirmation_token"],
54
+ },
55
+ };
56
+ export const renewToolSchema = {
57
+ name: "renew_cancel_timer",
58
+ description: "Resets the dead man's switch TTL for a market, preventing automatic order cancellation. " +
59
+ "No confirmation required. Requires an active timer set by schedule_cancel_all. " +
60
+ "Requires BUDA_API_KEY and BUDA_API_SECRET environment variables.",
61
+ inputSchema: {
62
+ type: "object",
63
+ properties: {
64
+ market_id: {
65
+ type: "string",
66
+ description: "Market ID whose timer should be renewed (e.g. 'BTC-CLP').",
67
+ },
68
+ },
69
+ required: ["market_id"],
70
+ },
71
+ };
72
+ export const disarmToolSchema = {
73
+ name: "disarm_cancel_timer",
74
+ description: "Disarms the dead man's switch for a market without cancelling any orders. " +
75
+ "No confirmation required. Safe to call even if no timer is active. " +
76
+ "Requires BUDA_API_KEY and BUDA_API_SECRET environment variables.",
77
+ inputSchema: {
78
+ type: "object",
79
+ properties: {
80
+ market_id: {
81
+ type: "string",
82
+ description: "Market ID whose timer should be disarmed (e.g. 'BTC-CLP').",
83
+ },
84
+ },
85
+ required: ["market_id"],
86
+ },
87
+ };
88
+ export async function handleScheduleCancelAll(args, client) {
89
+ const { market_id, ttl_seconds, confirmation_token } = args;
90
+ if (confirmation_token !== "CONFIRM") {
91
+ return {
92
+ content: [
93
+ {
94
+ type: "text",
95
+ text: JSON.stringify({
96
+ error: "Dead man's switch not armed. confirmation_token must equal 'CONFIRM' to activate. " +
97
+ "Review the parameters and set confirmation_token='CONFIRM' to proceed.",
98
+ code: "CONFIRMATION_REQUIRED",
99
+ market_id,
100
+ ttl_seconds,
101
+ }),
102
+ },
103
+ ],
104
+ isError: true,
105
+ };
106
+ }
107
+ const validationError = validateMarketId(market_id);
108
+ if (validationError) {
109
+ return {
110
+ content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_MARKET_ID" }) }],
111
+ isError: true,
112
+ };
113
+ }
114
+ const id = market_id.toLowerCase();
115
+ const entry = armTimer(id, ttl_seconds, client);
116
+ return {
117
+ content: [
118
+ {
119
+ type: "text",
120
+ text: JSON.stringify({
121
+ active: true,
122
+ market_id: market_id.toUpperCase(),
123
+ expires_at: new Date(entry.expiresAt).toISOString(),
124
+ ttl_seconds,
125
+ warning: "in-memory only — timer is lost on server restart. Not suitable for hosted deployments.",
126
+ }),
127
+ },
128
+ ],
129
+ };
130
+ }
131
+ export function handleRenewCancelTimer({ market_id }, client) {
132
+ const validationError = validateMarketId(market_id);
133
+ if (validationError) {
134
+ return {
135
+ content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_MARKET_ID" }) }],
136
+ isError: true,
137
+ };
138
+ }
139
+ const id = market_id.toLowerCase();
140
+ const existing = timers.get(id);
141
+ if (!existing) {
142
+ return {
143
+ content: [
144
+ {
145
+ type: "text",
146
+ text: JSON.stringify({
147
+ error: `No active dead man's switch for market ${market_id.toUpperCase()}. Arm one first with schedule_cancel_all.`,
148
+ code: "NO_ACTIVE_TIMER",
149
+ market_id: market_id.toUpperCase(),
150
+ }),
151
+ },
152
+ ],
153
+ isError: true,
154
+ };
155
+ }
156
+ const entry = armTimer(id, existing.ttlSeconds, client);
157
+ return {
158
+ content: [
159
+ {
160
+ type: "text",
161
+ text: JSON.stringify({
162
+ active: true,
163
+ market_id: market_id.toUpperCase(),
164
+ expires_at: new Date(entry.expiresAt).toISOString(),
165
+ ttl_seconds: entry.ttlSeconds,
166
+ }),
167
+ },
168
+ ],
169
+ };
170
+ }
171
+ export function handleDisarmCancelTimer({ market_id }) {
172
+ const validationError = validateMarketId(market_id);
173
+ if (validationError) {
174
+ return {
175
+ content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_MARKET_ID" }) }],
176
+ isError: true,
177
+ };
178
+ }
179
+ const id = market_id.toLowerCase();
180
+ const existing = timers.get(id);
181
+ if (!existing) {
182
+ return {
183
+ content: [
184
+ {
185
+ type: "text",
186
+ text: JSON.stringify({
187
+ disarmed: false,
188
+ market_id: market_id.toUpperCase(),
189
+ note: "No active timer for this market.",
190
+ }),
191
+ },
192
+ ],
193
+ };
194
+ }
195
+ clearTimeout(existing.timeout);
196
+ timers.delete(id);
197
+ return {
198
+ content: [
199
+ {
200
+ type: "text",
201
+ text: JSON.stringify({
202
+ disarmed: true,
203
+ market_id: market_id.toUpperCase(),
204
+ }),
205
+ },
206
+ ],
207
+ };
208
+ }
209
+ // ---- Registration ----
210
+ export function register(server, client) {
211
+ server.tool(toolSchema.name, toolSchema.description, {
212
+ market_id: z
213
+ .string()
214
+ .describe("Market ID to protect (e.g. 'BTC-CLP')."),
215
+ ttl_seconds: z
216
+ .number()
217
+ .int()
218
+ .min(10)
219
+ .max(300)
220
+ .describe("Seconds before all orders are cancelled if not renewed (10–300)."),
221
+ confirmation_token: z
222
+ .string()
223
+ .describe("Must equal exactly 'CONFIRM' (case-sensitive) to arm the switch."),
224
+ }, (args) => handleScheduleCancelAll(args, client));
225
+ server.tool(renewToolSchema.name, renewToolSchema.description, {
226
+ market_id: z
227
+ .string()
228
+ .describe("Market ID whose timer should be renewed (e.g. 'BTC-CLP')."),
229
+ }, (args) => handleRenewCancelTimer(args, client));
230
+ server.tool(disarmToolSchema.name, disarmToolSchema.description, {
231
+ market_id: z
232
+ .string()
233
+ .describe("Market ID whose timer should be disarmed (e.g. 'BTC-CLP')."),
234
+ }, (args) => handleDisarmCancelTimer(args));
235
+ }
236
+ //# sourceMappingURL=dead_mans_switch.js.map
@@ -0,0 +1,30 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { BudaClient } from "../client.js";
3
+ import { MemoryCache } from "../cache.js";
4
+ export declare const toolSchema: {
5
+ name: string;
6
+ description: string;
7
+ inputSchema: {
8
+ type: "object";
9
+ properties: {
10
+ market_id: {
11
+ type: string;
12
+ description: string;
13
+ };
14
+ };
15
+ required: string[];
16
+ };
17
+ };
18
+ type MarketSentimentArgs = {
19
+ market_id: string;
20
+ };
21
+ export declare function handleMarketSentiment({ market_id }: MarketSentimentArgs, client: BudaClient, cache: MemoryCache): Promise<{
22
+ content: Array<{
23
+ type: "text";
24
+ text: string;
25
+ }>;
26
+ isError?: boolean;
27
+ }>;
28
+ export declare function register(server: McpServer, client: BudaClient, cache: MemoryCache): void;
29
+ export {};
30
+ //# sourceMappingURL=market_sentiment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"market_sentiment.d.ts","sourceRoot":"","sources":["../../src/tools/market_sentiment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,UAAU,EAAgB,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,WAAW,EAAa,MAAM,aAAa,CAAC;AAIrD,eAAO,MAAM,UAAU;;;;;;;;;;;;;CAiBtB,CAAC;AAUF,KAAK,mBAAmB,GAAG;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjD,wBAAsB,qBAAqB,CACzC,EAAE,SAAS,EAAE,EAAE,mBAAmB,EAClC,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAuFhF;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAWxF"}