bykaranteli-mcp 0.2.0 → 0.3.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.
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * bykaranteli-mcp · MCP server for live crypto derivatives data
4
+ *
5
+ * Wraps the free, no-auth public JSON API at https://bykaranteli.com
6
+ * (docs: https://bykaranteli.com/developers). All data is Binance USDT-M
7
+ * perpetuals unless a tool says otherwise. Every response carries a
8
+ * generated-at timestamp; quote it when citing values.
9
+ */
10
+ export {};
package/dist/index.js CHANGED
@@ -10,327 +10,16 @@
10
10
  import { createRequire } from "node:module";
11
11
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
- import { z } from "zod";
13
+ import { registerByKaranteliTools } from "./tools.js";
14
14
  // Single source of truth for the version: package.json ships in the tarball
15
15
  // and sits one level above both src/ and dist/.
16
16
  const { version: VERSION } = createRequire(import.meta.url)("../package.json");
17
17
  const BASE_URL = (process.env.BYKARANTELI_BASE_URL ?? "https://bykaranteli.com").replace(/\/+$/, "");
18
- const USER_AGENT = `bykaranteli-mcp/${VERSION} (+https://github.com/bykarantelicom/bykaranteli-mcp)`;
19
- const TIMEOUT_MS = 15_000;
20
- // Read-only GET tools over an open-world public API, all of them.
21
- const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
22
- /** Invalid tool input (not a network problem): no retry hint in the error. */
23
- class InputError extends Error {
24
- }
25
- async function fetchJson(path) {
26
- const controller = new AbortController();
27
- const timer = setTimeout(() => controller.abort(new Error(`${path} timed out after ${TIMEOUT_MS / 1000}s`)), TIMEOUT_MS);
28
- try {
29
- const res = await fetch(`${BASE_URL}${path}`, {
30
- signal: controller.signal,
31
- headers: { accept: "application/json", "user-agent": USER_AGENT },
32
- });
33
- if (!res.ok) {
34
- throw new Error(`${path} returned HTTP ${res.status}`);
35
- }
36
- return await res.json();
37
- }
38
- finally {
39
- clearTimeout(timer);
40
- }
41
- }
42
- function ok(data) {
43
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
44
- }
45
- function fail(err) {
46
- const message = err instanceof Error ? err.message : String(err);
47
- const text = err instanceof InputError
48
- ? `Invalid input: ${message}`
49
- : `Error fetching data from bykaranteli.com: ${message}. The API is free and unauthenticated; transient errors usually resolve on retry.`;
50
- return {
51
- content: [{ type: "text", text }],
52
- isError: true,
53
- };
54
- }
55
- const SYMBOL_RE = /^[A-Z0-9]{5,20}$/;
56
- function normalizeSymbol(raw) {
57
- // Accept the spellings models actually produce: "BTC/USDT", "btc-usdt",
58
- // "BTC USDT", bare "BTC". Strip separators, then expand to the USDT perp.
59
- const s = raw.trim().toUpperCase().replace(/[\s/_-]+/g, "");
60
- const expanded = s.endsWith("USDT") ? s : `${s}USDT`;
61
- if (!SYMBOL_RE.test(expanded)) {
62
- throw new InputError(`"${raw}" is not a valid symbol. Use a Binance USDT-M perp symbol like BTCUSDT or a coin name like BTC.`);
63
- }
64
- return expanded;
65
- }
66
18
  const server = new McpServer({
67
19
  name: "bykaranteli",
68
20
  version: VERSION,
69
21
  });
70
- server.registerTool("get_market_indices", {
71
- title: "Crypto market indices (Fear & Greed, BTC dominance, euphoria)",
72
- description: "Call this when the user asks about overall crypto market sentiment or macro state: the Fear & Greed index (today and yesterday), Bitcoin dominance percentage, total market cap, or the Retail Euphoria composite. Live values refreshed about every 30 minutes.",
73
- inputSchema: {},
74
- annotations: READ_ONLY,
75
- }, async () => {
76
- try {
77
- const d = (await fetchJson("/api/public/indices"));
78
- // Trim heavy internals (weights/contributions) but keep the readable parts.
79
- const euphoria = d.euphoria;
80
- return ok({
81
- generatedAt: d.generatedAt,
82
- fearGreed: d.fearGreed,
83
- global: d.global,
84
- stablecoins: d.stablecoins,
85
- euphoria: euphoria
86
- ? {
87
- score: euphoria.score,
88
- regime: euphoria.regime,
89
- regimeLabel: euphoria.regimeLabel,
90
- explainer: euphoria.explainer,
91
- }
92
- : undefined,
93
- source: `${BASE_URL}/indices`,
94
- });
95
- }
96
- catch (err) {
97
- return fail(err);
98
- }
99
- });
100
- server.registerTool("get_liquidations", {
101
- title: "Crypto liquidations: daily long/short totals per symbol and exchange",
102
- description: "Call this when the user asks how much was liquidated in crypto futures, whether longs or shorts got flushed, or for liquidation history. Returns daily long and short liquidation totals in USD per symbol and exchange, recorded from ByKaranteli's own Binance, Bybit and OKX stream collectors (recorded events, a floor, not estimates). One row per finalized UTC day, symbol and exchange; history begins 2026-07-30 and grows daily.",
103
- inputSchema: {
104
- symbol: z
105
- .string()
106
- .trim()
107
- .toUpperCase()
108
- .regex(/^[A-Z0-9]{2,20}$/)
109
- .optional()
110
- .describe("Optional symbol filter like BTCUSDT or ETHUSDT. Omit for all symbols."),
111
- days: z.number().int().min(1).max(90).optional().describe("How many most recent days to return (default 7)."),
112
- },
113
- annotations: READ_ONLY,
114
- }, async ({ symbol, days }) => {
115
- try {
116
- const d = (await fetchJson("/api/v1/public/datasets/liquidations-daily.json"));
117
- const rows = Array.isArray(d.rows) ? d.rows : [];
118
- const wantDays = days ?? 7;
119
- const dates = [...new Set(rows.map((r) => String(r.date)))].sort().reverse().slice(0, wantDays);
120
- const dateSet = new Set(dates);
121
- const filtered = rows.filter((r) => dateSet.has(String(r.date)) && (!symbol || String(r.symbol).toUpperCase() === symbol));
122
- return ok({
123
- rows: filtered.slice(0, 400),
124
- note: "Recorded from public exchange streams; totals are a floor (Binance throttles its stream). Live 24h view: " + BASE_URL + "/liquidations",
125
- source: `${BASE_URL}/liquidations`,
126
- });
127
- }
128
- catch (err) {
129
- return fail(err);
130
- }
131
- });
132
- server.registerTool("get_etf_flows", {
133
- title: "US spot Bitcoin and Ethereum ETF daily flows",
134
- description: "Call this when the user asks about Bitcoin or Ethereum ETF flows: daily net inflows or outflows, cumulative flow since launch, or total net assets of the US spot ETFs (IBIT, FBTC, ETHA and the rest). Returns one row per finalized US trading day and asset with net inflow, total net assets, cumulative inflow and value traded, all in USD. About 14 months of history.",
135
- inputSchema: {
136
- asset: z.enum(["BTC", "ETH"]).optional().describe("Filter to one asset. Omit for both."),
137
- days: z.number().int().min(1).max(400).optional().describe("How many most recent trading days to return (default 10)."),
138
- },
139
- annotations: READ_ONLY,
140
- }, async ({ asset, days }) => {
141
- try {
142
- const d = (await fetchJson("/api/v1/public/datasets/etf-flows.json"));
143
- const rows = Array.isArray(d.rows) ? d.rows : [];
144
- const filtered = rows.filter((r) => !asset || String(r.asset) === asset);
145
- return ok({
146
- rows: filtered.slice(0, (days ?? 10) * (asset ? 1 : 2)),
147
- note: "Finalized US trading days only; a positive net_inflow_usd means the funds bought more of the asset than they sold that day.",
148
- source: `${BASE_URL}/etf`,
149
- });
150
- }
151
- catch (err) {
152
- return fail(err);
153
- }
154
- });
155
- server.registerTool("get_funding_heatmap", {
156
- title: "Funding rates across top-30 Binance perps",
157
- description: "Call this when the user asks for the full current funding table across the top-30 Binance perp universe, or the funding rate of one specific coin. For a pre-ranked top-10 of the most extreme funding rates, use get_top_movers instead. Returns per-symbol funding rate (per settlement interval), 24h open interest change and 24h price change for the top-30 Binance USDT-M perpetuals. Positive funding means longs pay shorts.",
158
- inputSchema: {
159
- symbol: z
160
- .string()
161
- .optional()
162
- .describe("Optional. Filter to one symbol, e.g. BTCUSDT or just BTC. Omit to get all 30 rows."),
163
- },
164
- annotations: READ_ONLY,
165
- }, async ({ symbol }) => {
166
- try {
167
- const d = (await fetchJson("/api/public/heatmap"));
168
- if (symbol) {
169
- const want = normalizeSymbol(symbol);
170
- const row = d.rows.find((r) => r.symbol === want);
171
- if (!row) {
172
- return ok({
173
- generatedAt: d.generatedAt,
174
- note: `${want} is not in the top-30 heatmap set. Tracked symbols: ${d.rows.map((r) => r.symbol).join(", ")}`,
175
- });
176
- }
177
- return ok({ generatedAt: d.generatedAt, row, source: `${BASE_URL}/heatmap` });
178
- }
179
- return ok({ ...d, source: `${BASE_URL}/heatmap` });
180
- }
181
- catch (err) {
182
- return fail(err);
183
- }
184
- });
185
- server.registerTool("get_funding_arbitrage", {
186
- title: "Cross-exchange funding arbitrage opportunities",
187
- description: "Call this when the user asks about funding arbitrage, funding rate differences between exchanges, or delta-neutral carry trades. Compares funding across Binance, OKX, Bybit, Gate, HTX and BingX for 12 major perps and returns the best long/short venue per symbol with gross and net annualized APR (net of taker fees and weekly rebalance cost).",
188
- inputSchema: {},
189
- annotations: READ_ONLY,
190
- }, async () => {
191
- try {
192
- const d = (await fetchJson("/api/public/funding-arb"));
193
- return ok({ ...d, source: `${BASE_URL}/funding-arb` });
194
- }
195
- catch (err) {
196
- return fail(err);
197
- }
198
- });
199
- server.registerTool("get_pressure_scores", {
200
- title: "Derivatives pressure scores (funding + OI + basis composite)",
201
- description: "Call this when the user asks which coins are crowded or over-leveraged, or asks for the pressure/derivatives-stress score of specific coins. For a quick top-10 ranking of the highest-stress coins right now, use get_top_movers instead. Each symbol gets a 0-100 composite score built from funding rate, 1h/4h/24h open interest deltas and basis, with a LONG/SHORT/NEUTRAL direction and a plain-language regime label.",
202
- inputSchema: {
203
- symbol: z
204
- .string()
205
- .optional()
206
- .describe("Optional. Return only this symbol, e.g. BTCUSDT or BTC."),
207
- limit: z.coerce
208
- .number()
209
- .int()
210
- .min(1)
211
- .max(50)
212
- .optional()
213
- .describe("Optional. Max rows to return when no symbol filter is set (default 20, sorted by score)."),
214
- },
215
- annotations: READ_ONLY,
216
- }, async ({ symbol, limit }) => {
217
- try {
218
- const d = (await fetchJson("/api/public/pressure"));
219
- const slim = (it) => ({
220
- symbol: it.symbol,
221
- score: it.score,
222
- direction: it.direction,
223
- regimeLabel: it.regimeLabel,
224
- components: it.components,
225
- explainer: it.explainer,
226
- });
227
- if (symbol) {
228
- const want = normalizeSymbol(symbol);
229
- const item = d.items.find((i) => i.symbol === want);
230
- if (!item) {
231
- return ok({
232
- generatedAt: d.generatedAt,
233
- note: `${want} is not in the tracked pressure universe right now.`,
234
- });
235
- }
236
- return ok({ generatedAt: d.generatedAt, item: slim(item), source: `${BASE_URL}/pressure` });
237
- }
238
- return ok({
239
- generatedAt: d.generatedAt,
240
- items: d.items.slice(0, limit ?? 20).map(slim),
241
- totalTracked: d.items.length,
242
- source: `${BASE_URL}/pressure`,
243
- });
244
- }
245
- catch (err) {
246
- return fail(err);
247
- }
248
- });
249
- server.registerTool("get_top_movers", {
250
- title: "Top movers: OI spikes, extreme funding, widest basis, highest stress",
251
- description: "Call this when the user asks what is moving in crypto derivatives right now, which coins have the biggest open interest changes, the most extreme funding, the widest basis, or the highest derivatives stress. Returns four top-10 lists in one call.",
252
- inputSchema: {},
253
- annotations: READ_ONLY,
254
- }, async () => {
255
- try {
256
- const d = (await fetchJson("/api/public/top-movers"));
257
- return ok({ ...d, source: `${BASE_URL}/top-movers` });
258
- }
259
- catch (err) {
260
- return fail(err);
261
- }
262
- });
263
- server.registerTool("get_recent_signals", {
264
- title: "Recent closed trading signals with verified outcomes",
265
- description: "Call this when the user asks how the ByKaranteli signal engine is doing today, or wants recent closed LONG/SHORT signals with real outcomes (TP1, SL or TIMEOUT) and net basis-point results. Includes a 24h summary (wins, losses, net bps). Every signal is published with a SHA-256 receipt and results are net of fees, slippage and funding; live signals only, never backtests.",
266
- inputSchema: {},
267
- annotations: READ_ONLY,
268
- }, async () => {
269
- try {
270
- const d = (await fetchJson("/api/public/recent"));
271
- return ok({ ...d, source: `${BASE_URL}/signals` });
272
- }
273
- catch (err) {
274
- return fail(err);
275
- }
276
- });
277
- server.registerTool("get_symbol_performance", {
278
- title: "Per-symbol signal performance and recent trades",
279
- description: "Call this when the user asks how signals performed on a specific coin (win rate, profit factor, net PnL, best/worst trade) or wants that coin's recent closed signals. Data is the live verified track record for one Binance USDT-M perp over a 30, 90 or 180 day window.",
280
- inputSchema: {
281
- symbol: z.string().describe("The symbol, e.g. BTCUSDT, or a coin name like BTC."),
282
- window_days: z
283
- .preprocess((v) => (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v)) ? Number(v) : v), z.union([z.literal(30), z.literal(90), z.literal(180)]))
284
- .optional()
285
- .describe("Optional lookback window in days (30, 90 or 180). Default 90."),
286
- },
287
- annotations: READ_ONLY,
288
- }, async ({ symbol, window_days }) => {
289
- try {
290
- const want = normalizeSymbol(symbol);
291
- const w = window_days ?? 90;
292
- let d;
293
- try {
294
- d = (await fetchJson(`/api/v1/public/symbols/${want}?window=${w}`));
295
- }
296
- catch (err) {
297
- // The API 404s when the symbol has no closed signals in the chosen
298
- // window (data-dependent, not an invalid request). Say so plainly.
299
- if (err instanceof Error && err.message.includes("HTTP 404")) {
300
- return ok({
301
- symbol: want,
302
- window_days: w,
303
- note: `No closed signals recorded for ${want} in the last ${w} days (or the symbol is not tracked). Try window_days: 180, or check ${BASE_URL}/symbols/${want}.`,
304
- });
305
- }
306
- throw err;
307
- }
308
- // daily_points can be long; the stats + recent signals carry the answer.
309
- const daily = d.daily_points;
310
- return ok({
311
- ...d,
312
- daily_points: Array.isArray(daily) ? daily.slice(-30) : daily,
313
- source: `${BASE_URL}/symbols/${want}`,
314
- });
315
- }
316
- catch (err) {
317
- return fail(err);
318
- }
319
- });
320
- server.registerTool("get_strategy_leaderboard", {
321
- title: "Strategy leaderboard with verified live results",
322
- description: "Call this when the user asks which trading strategies are performing best, or wants win rate, profit factor, drawdown and Sharpe per strategy. Rankings are computed from live closed signals only (no backtests), net of fees.",
323
- inputSchema: {},
324
- annotations: READ_ONLY,
325
- }, async () => {
326
- try {
327
- const d = (await fetchJson("/api/v1/public/leaderboard"));
328
- return ok({ ...d, source: `${BASE_URL}/leaderboard` });
329
- }
330
- catch (err) {
331
- return fail(err);
332
- }
333
- });
22
+ registerByKaranteliTools(server, { version: VERSION });
334
23
  async function main() {
335
24
  // If the host dies while a response is in flight, exit quietly instead of
336
25
  // crashing with an unhandled EPIPE from the stdout pipe. Non-EPIPE stream
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Tool registrations for the ByKaranteli MCP server, transport-agnostic.
3
+ * The stdio bin (index.ts) and the hosted Streamable HTTP endpoint on
4
+ * bykaranteli.com both call registerByKaranteliTools on their own server
5
+ * instance, so the 10 tools have exactly one definition.
6
+ */
7
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ export type RegisterOptions = {
9
+ /** Override the public API origin (hosted endpoint uses the internal one). */
10
+ baseUrl?: string;
11
+ /** Version string for the outbound user-agent. */
12
+ version?: string;
13
+ };
14
+ export declare function registerByKaranteliTools(server: McpServer, options?: RegisterOptions): void;
package/dist/tools.js ADDED
@@ -0,0 +1,322 @@
1
+ /**
2
+ * Tool registrations for the ByKaranteli MCP server, transport-agnostic.
3
+ * The stdio bin (index.ts) and the hosted Streamable HTTP endpoint on
4
+ * bykaranteli.com both call registerByKaranteliTools on their own server
5
+ * instance, so the 10 tools have exactly one definition.
6
+ */
7
+ import { z } from "zod";
8
+ export function registerByKaranteliTools(server, options) {
9
+ const BASE_URL = (options?.baseUrl ?? process.env.BYKARANTELI_BASE_URL ?? "https://bykaranteli.com").replace(/\/+$/, "");
10
+ const USER_AGENT = `bykaranteli-mcp/${options?.version ?? "dev"} (+https://github.com/bykarantelicom/bykaranteli-mcp)`;
11
+ const TIMEOUT_MS = 15_000;
12
+ // Read-only GET tools over an open-world public API, all of them.
13
+ const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
14
+ /** Invalid tool input (not a network problem): no retry hint in the error. */
15
+ class InputError extends Error {
16
+ }
17
+ async function fetchJson(path) {
18
+ const controller = new AbortController();
19
+ const timer = setTimeout(() => controller.abort(new Error(`${path} timed out after ${TIMEOUT_MS / 1000}s`)), TIMEOUT_MS);
20
+ try {
21
+ const res = await fetch(`${BASE_URL}${path}`, {
22
+ signal: controller.signal,
23
+ headers: { accept: "application/json", "user-agent": USER_AGENT },
24
+ });
25
+ if (!res.ok) {
26
+ throw new Error(`${path} returned HTTP ${res.status}`);
27
+ }
28
+ return await res.json();
29
+ }
30
+ finally {
31
+ clearTimeout(timer);
32
+ }
33
+ }
34
+ function ok(data) {
35
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
36
+ }
37
+ function fail(err) {
38
+ const message = err instanceof Error ? err.message : String(err);
39
+ const text = err instanceof InputError
40
+ ? `Invalid input: ${message}`
41
+ : `Error fetching data from bykaranteli.com: ${message}. The API is free and unauthenticated; transient errors usually resolve on retry.`;
42
+ return {
43
+ content: [{ type: "text", text }],
44
+ isError: true,
45
+ };
46
+ }
47
+ const SYMBOL_RE = /^[A-Z0-9]{5,20}$/;
48
+ function normalizeSymbol(raw) {
49
+ // Accept the spellings models actually produce: "BTC/USDT", "btc-usdt",
50
+ // "BTC USDT", bare "BTC". Strip separators, then expand to the USDT perp.
51
+ const s = raw.trim().toUpperCase().replace(/[\s/_-]+/g, "");
52
+ const expanded = s.endsWith("USDT") ? s : `${s}USDT`;
53
+ if (!SYMBOL_RE.test(expanded)) {
54
+ throw new InputError(`"${raw}" is not a valid symbol. Use a Binance USDT-M perp symbol like BTCUSDT or a coin name like BTC.`);
55
+ }
56
+ return expanded;
57
+ }
58
+ server.registerTool("get_market_indices", {
59
+ title: "Crypto market indices (Fear & Greed, BTC dominance, euphoria)",
60
+ description: "Call this when the user asks about overall crypto market sentiment or macro state: the Fear & Greed index (today and yesterday), Bitcoin dominance percentage, total market cap, or the Retail Euphoria composite. Live values refreshed about every 30 minutes.",
61
+ inputSchema: {},
62
+ annotations: READ_ONLY,
63
+ }, async () => {
64
+ try {
65
+ const d = (await fetchJson("/api/public/indices"));
66
+ // Trim heavy internals (weights/contributions) but keep the readable parts.
67
+ const euphoria = d.euphoria;
68
+ return ok({
69
+ generatedAt: d.generatedAt,
70
+ fearGreed: d.fearGreed,
71
+ global: d.global,
72
+ stablecoins: d.stablecoins,
73
+ euphoria: euphoria
74
+ ? {
75
+ score: euphoria.score,
76
+ regime: euphoria.regime,
77
+ regimeLabel: euphoria.regimeLabel,
78
+ explainer: euphoria.explainer,
79
+ }
80
+ : undefined,
81
+ source: `${BASE_URL}/indices`,
82
+ });
83
+ }
84
+ catch (err) {
85
+ return fail(err);
86
+ }
87
+ });
88
+ server.registerTool("get_liquidations", {
89
+ title: "Crypto liquidations: daily long/short totals per symbol and exchange",
90
+ description: "Call this when the user asks how much was liquidated in crypto futures, whether longs or shorts got flushed, or for liquidation history. Returns daily long and short liquidation totals in USD per symbol and exchange, recorded from ByKaranteli's own Binance, Bybit and OKX stream collectors (recorded events, a floor, not estimates). One row per finalized UTC day, symbol and exchange; history begins 2026-07-30 and grows daily.",
91
+ inputSchema: {
92
+ symbol: z
93
+ .string()
94
+ .trim()
95
+ .toUpperCase()
96
+ .regex(/^[A-Z0-9]{2,20}$/)
97
+ .optional()
98
+ .describe("Optional symbol filter like BTCUSDT or ETHUSDT. Omit for all symbols."),
99
+ days: z.number().int().min(1).max(90).optional().describe("How many most recent days to return (default 7)."),
100
+ },
101
+ annotations: READ_ONLY,
102
+ }, async ({ symbol, days }) => {
103
+ try {
104
+ const d = (await fetchJson("/api/v1/public/datasets/liquidations-daily.json"));
105
+ const rows = Array.isArray(d.rows) ? d.rows : [];
106
+ const wantDays = days ?? 7;
107
+ const dates = [...new Set(rows.map((r) => String(r.date)))].sort().reverse().slice(0, wantDays);
108
+ const dateSet = new Set(dates);
109
+ const filtered = rows.filter((r) => dateSet.has(String(r.date)) && (!symbol || String(r.symbol).toUpperCase() === symbol));
110
+ return ok({
111
+ rows: filtered.slice(0, 400),
112
+ note: "Recorded from public exchange streams; totals are a floor (Binance throttles its stream). Live 24h view: " + BASE_URL + "/liquidations",
113
+ source: `${BASE_URL}/liquidations`,
114
+ });
115
+ }
116
+ catch (err) {
117
+ return fail(err);
118
+ }
119
+ });
120
+ server.registerTool("get_etf_flows", {
121
+ title: "US spot Bitcoin and Ethereum ETF daily flows",
122
+ description: "Call this when the user asks about Bitcoin or Ethereum ETF flows: daily net inflows or outflows, cumulative flow since launch, or total net assets of the US spot ETFs (IBIT, FBTC, ETHA and the rest). Returns one row per finalized US trading day and asset with net inflow, total net assets, cumulative inflow and value traded, all in USD. About 14 months of history.",
123
+ inputSchema: {
124
+ asset: z.enum(["BTC", "ETH"]).optional().describe("Filter to one asset. Omit for both."),
125
+ days: z.number().int().min(1).max(400).optional().describe("How many most recent trading days to return (default 10)."),
126
+ },
127
+ annotations: READ_ONLY,
128
+ }, async ({ asset, days }) => {
129
+ try {
130
+ const d = (await fetchJson("/api/v1/public/datasets/etf-flows.json"));
131
+ const rows = Array.isArray(d.rows) ? d.rows : [];
132
+ const filtered = rows.filter((r) => !asset || String(r.asset) === asset);
133
+ return ok({
134
+ rows: filtered.slice(0, (days ?? 10) * (asset ? 1 : 2)),
135
+ note: "Finalized US trading days only; a positive net_inflow_usd means the funds bought more of the asset than they sold that day.",
136
+ source: `${BASE_URL}/etf`,
137
+ });
138
+ }
139
+ catch (err) {
140
+ return fail(err);
141
+ }
142
+ });
143
+ server.registerTool("get_funding_heatmap", {
144
+ title: "Funding rates across top-30 Binance perps",
145
+ description: "Call this when the user asks for the full current funding table across the top-30 Binance perp universe, or the funding rate of one specific coin. For a pre-ranked top-10 of the most extreme funding rates, use get_top_movers instead. Returns per-symbol funding rate (per settlement interval), 24h open interest change and 24h price change for the top-30 Binance USDT-M perpetuals. Positive funding means longs pay shorts.",
146
+ inputSchema: {
147
+ symbol: z
148
+ .string()
149
+ .optional()
150
+ .describe("Optional. Filter to one symbol, e.g. BTCUSDT or just BTC. Omit to get all 30 rows."),
151
+ },
152
+ annotations: READ_ONLY,
153
+ }, async ({ symbol }) => {
154
+ try {
155
+ const d = (await fetchJson("/api/public/heatmap"));
156
+ if (symbol) {
157
+ const want = normalizeSymbol(symbol);
158
+ const row = d.rows.find((r) => r.symbol === want);
159
+ if (!row) {
160
+ return ok({
161
+ generatedAt: d.generatedAt,
162
+ note: `${want} is not in the top-30 heatmap set. Tracked symbols: ${d.rows.map((r) => r.symbol).join(", ")}`,
163
+ });
164
+ }
165
+ return ok({ generatedAt: d.generatedAt, row, source: `${BASE_URL}/heatmap` });
166
+ }
167
+ return ok({ ...d, source: `${BASE_URL}/heatmap` });
168
+ }
169
+ catch (err) {
170
+ return fail(err);
171
+ }
172
+ });
173
+ server.registerTool("get_funding_arbitrage", {
174
+ title: "Cross-exchange funding arbitrage opportunities",
175
+ description: "Call this when the user asks about funding arbitrage, funding rate differences between exchanges, or delta-neutral carry trades. Compares funding across Binance, OKX, Bybit, Gate, HTX and BingX for 12 major perps and returns the best long/short venue per symbol with gross and net annualized APR (net of taker fees and weekly rebalance cost).",
176
+ inputSchema: {},
177
+ annotations: READ_ONLY,
178
+ }, async () => {
179
+ try {
180
+ const d = (await fetchJson("/api/public/funding-arb"));
181
+ return ok({ ...d, source: `${BASE_URL}/funding-arb` });
182
+ }
183
+ catch (err) {
184
+ return fail(err);
185
+ }
186
+ });
187
+ server.registerTool("get_pressure_scores", {
188
+ title: "Derivatives pressure scores (funding + OI + basis composite)",
189
+ description: "Call this when the user asks which coins are crowded or over-leveraged, or asks for the pressure/derivatives-stress score of specific coins. For a quick top-10 ranking of the highest-stress coins right now, use get_top_movers instead. Each symbol gets a 0-100 composite score built from funding rate, 1h/4h/24h open interest deltas and basis, with a LONG/SHORT/NEUTRAL direction and a plain-language regime label.",
190
+ inputSchema: {
191
+ symbol: z
192
+ .string()
193
+ .optional()
194
+ .describe("Optional. Return only this symbol, e.g. BTCUSDT or BTC."),
195
+ limit: z.coerce
196
+ .number()
197
+ .int()
198
+ .min(1)
199
+ .max(50)
200
+ .optional()
201
+ .describe("Optional. Max rows to return when no symbol filter is set (default 20, sorted by score)."),
202
+ },
203
+ annotations: READ_ONLY,
204
+ }, async ({ symbol, limit }) => {
205
+ try {
206
+ const d = (await fetchJson("/api/public/pressure"));
207
+ const slim = (it) => ({
208
+ symbol: it.symbol,
209
+ score: it.score,
210
+ direction: it.direction,
211
+ regimeLabel: it.regimeLabel,
212
+ components: it.components,
213
+ explainer: it.explainer,
214
+ });
215
+ if (symbol) {
216
+ const want = normalizeSymbol(symbol);
217
+ const item = d.items.find((i) => i.symbol === want);
218
+ if (!item) {
219
+ return ok({
220
+ generatedAt: d.generatedAt,
221
+ note: `${want} is not in the tracked pressure universe right now.`,
222
+ });
223
+ }
224
+ return ok({ generatedAt: d.generatedAt, item: slim(item), source: `${BASE_URL}/pressure` });
225
+ }
226
+ return ok({
227
+ generatedAt: d.generatedAt,
228
+ items: d.items.slice(0, limit ?? 20).map(slim),
229
+ totalTracked: d.items.length,
230
+ source: `${BASE_URL}/pressure`,
231
+ });
232
+ }
233
+ catch (err) {
234
+ return fail(err);
235
+ }
236
+ });
237
+ server.registerTool("get_top_movers", {
238
+ title: "Top movers: OI spikes, extreme funding, widest basis, highest stress",
239
+ description: "Call this when the user asks what is moving in crypto derivatives right now, which coins have the biggest open interest changes, the most extreme funding, the widest basis, or the highest derivatives stress. Returns four top-10 lists in one call.",
240
+ inputSchema: {},
241
+ annotations: READ_ONLY,
242
+ }, async () => {
243
+ try {
244
+ const d = (await fetchJson("/api/public/top-movers"));
245
+ return ok({ ...d, source: `${BASE_URL}/top-movers` });
246
+ }
247
+ catch (err) {
248
+ return fail(err);
249
+ }
250
+ });
251
+ server.registerTool("get_recent_signals", {
252
+ title: "Recent closed trading signals with verified outcomes",
253
+ description: "Call this when the user asks how the ByKaranteli signal engine is doing today, or wants recent closed LONG/SHORT signals with real outcomes (TP1, SL or TIMEOUT) and net basis-point results. Includes a 24h summary (wins, losses, net bps). Every signal is published with a SHA-256 receipt and results are net of fees, slippage and funding; live signals only, never backtests.",
254
+ inputSchema: {},
255
+ annotations: READ_ONLY,
256
+ }, async () => {
257
+ try {
258
+ const d = (await fetchJson("/api/public/recent"));
259
+ return ok({ ...d, source: `${BASE_URL}/signals` });
260
+ }
261
+ catch (err) {
262
+ return fail(err);
263
+ }
264
+ });
265
+ server.registerTool("get_symbol_performance", {
266
+ title: "Per-symbol signal performance and recent trades",
267
+ description: "Call this when the user asks how signals performed on a specific coin (win rate, profit factor, net PnL, best/worst trade) or wants that coin's recent closed signals. Data is the live verified track record for one Binance USDT-M perp over a 30, 90 or 180 day window.",
268
+ inputSchema: {
269
+ symbol: z.string().describe("The symbol, e.g. BTCUSDT, or a coin name like BTC."),
270
+ window_days: z
271
+ .preprocess((v) => (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v)) ? Number(v) : v), z.union([z.literal(30), z.literal(90), z.literal(180)]))
272
+ .optional()
273
+ .describe("Optional lookback window in days (30, 90 or 180). Default 90."),
274
+ },
275
+ annotations: READ_ONLY,
276
+ }, async ({ symbol, window_days }) => {
277
+ try {
278
+ const want = normalizeSymbol(symbol);
279
+ const w = window_days ?? 90;
280
+ let d;
281
+ try {
282
+ d = (await fetchJson(`/api/v1/public/symbols/${want}?window=${w}`));
283
+ }
284
+ catch (err) {
285
+ // The API 404s when the symbol has no closed signals in the chosen
286
+ // window (data-dependent, not an invalid request). Say so plainly.
287
+ if (err instanceof Error && err.message.includes("HTTP 404")) {
288
+ return ok({
289
+ symbol: want,
290
+ window_days: w,
291
+ note: `No closed signals recorded for ${want} in the last ${w} days (or the symbol is not tracked). Try window_days: 180, or check ${BASE_URL}/symbols/${want}.`,
292
+ });
293
+ }
294
+ throw err;
295
+ }
296
+ // daily_points can be long; the stats + recent signals carry the answer.
297
+ const daily = d.daily_points;
298
+ return ok({
299
+ ...d,
300
+ daily_points: Array.isArray(daily) ? daily.slice(-30) : daily,
301
+ source: `${BASE_URL}/symbols/${want}`,
302
+ });
303
+ }
304
+ catch (err) {
305
+ return fail(err);
306
+ }
307
+ });
308
+ server.registerTool("get_strategy_leaderboard", {
309
+ title: "Strategy leaderboard with verified live results",
310
+ description: "Call this when the user asks which trading strategies are performing best, or wants win rate, profit factor, drawdown and Sharpe per strategy. Rankings are computed from live closed signals only (no backtests), net of fees.",
311
+ inputSchema: {},
312
+ annotations: READ_ONLY,
313
+ }, async () => {
314
+ try {
315
+ const d = (await fetchJson("/api/v1/public/leaderboard"));
316
+ return ok({ ...d, source: `${BASE_URL}/leaderboard` });
317
+ }
318
+ catch (err) {
319
+ return fail(err);
320
+ }
321
+ });
322
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bykaranteli-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server for live crypto derivatives data from bykaranteli.com: funding rates, funding arbitrage, open interest pressure, Fear & Greed, BTC dominance and verified signal performance. Free, no API key.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -53,5 +53,15 @@
53
53
  "@types/node": "^20.0.0",
54
54
  "typescript": "^5.5.0"
55
55
  },
56
- "mcpName": "com.bykaranteli/mcp"
56
+ "mcpName": "com.bykaranteli/mcp",
57
+ "exports": {
58
+ ".": {
59
+ "types": "./dist/tools.d.ts",
60
+ "default": "./dist/tools.js"
61
+ },
62
+ "./tools": {
63
+ "types": "./dist/tools.d.ts",
64
+ "default": "./dist/tools.js"
65
+ }
66
+ }
57
67
  }