bykaranteli-mcp 0.7.3 → 0.9.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 (3) hide show
  1. package/README.md +2 -2
  2. package/dist/tools.js +277 -19
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -7,7 +7,7 @@ MCP (Model Context Protocol) server for **live crypto derivatives data**: fundin
7
7
  ## Hosted endpoint (no install)
8
8
 
9
9
  Paste `https://mcp.bykaranteli.com` as a custom connector in any MCP-capable
10
- assistant. Same 20 tools, nothing to install, no key.
10
+ assistant. Same 23 tools, nothing to install, no key.
11
11
 
12
12
  ## Quick start
13
13
 
@@ -91,7 +91,7 @@ MIT. Attribution appreciated: "ByKaranteli (bykaranteli.com)".
91
91
 
92
92
  ## Paid depth (optional)
93
93
 
94
- The 20 tools above are free and stay free. For recorded history and raw records
94
+ The 23 tools above are free and stay free. For recorded history and raw records
95
95
  beyond the live snapshots, bykaranteli.com also exposes pay-per-call x402
96
96
  endpoints (USDC on Solana or Base, priced per call, no account):
97
97
  <https://bykaranteli.com/developers#x402> · machine catalog:
package/dist/tools.js CHANGED
@@ -24,7 +24,10 @@ export function registerByKaranteliTools(server, options) {
24
24
  headers: { accept: "application/json", "user-agent": USER_AGENT },
25
25
  });
26
26
  if (!res.ok) {
27
- throw new Error(`${path} returned HTTP ${res.status}`);
27
+ /* Carry the route's own error body (audit P3 #412): "HTTP 400" alone
28
+ * hid the actual reason (unknown metric, bad symbol...). */
29
+ const body = (await res.text().catch(() => "")).replace(/\s+/g, " ").slice(0, 240);
30
+ throw new Error(`${path} returned HTTP ${res.status}${body ? `: ${body}` : ""}`);
28
31
  }
29
32
  return await res.json();
30
33
  }
@@ -32,14 +35,29 @@ export function registerByKaranteliTools(server, options) {
32
35
  clearTimeout(timer);
33
36
  }
34
37
  }
38
+ /* Every answer carries where it came from and when (README promise; audit
39
+ * P3 #416): routes that already emit these keep their own values. */
40
+ function withProvenance(data, path) {
41
+ if (!data || typeof data !== "object" || Array.isArray(data))
42
+ return data;
43
+ const record = data;
44
+ return {
45
+ ...record,
46
+ generatedAt: typeof record.generatedAt === "string" ? record.generatedAt : new Date().toISOString(),
47
+ source: typeof record.source === "string" ? record.source : `${PUBLIC_URL}${path.replace(/^\/api\/public/, "").split("?")[0] || "/"}`,
48
+ };
49
+ }
35
50
  function ok(data) {
36
51
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
37
52
  }
38
53
  function fail(err) {
39
54
  const message = err instanceof Error ? err.message : String(err);
55
+ const permanent = /returned HTTP 4\d\d/.test(message);
40
56
  const text = err instanceof InputError
41
57
  ? `Invalid input: ${message}`
42
- : `Error fetching data from bykaranteli.com: ${message}. The API is free and unauthenticated; transient errors usually resolve on retry.`;
58
+ : permanent
59
+ ? `Error fetching data from bykaranteli.com: ${message}. This is a permanent error for these inputs (wrong parameter or route), not a transient one; retrying will not help.`
60
+ : `Error fetching data from bykaranteli.com: ${message}. The API is free and unauthenticated; transient errors usually resolve on retry.`;
43
61
  return {
44
62
  content: [{ type: "text", text }],
45
63
  isError: true,
@@ -88,7 +106,7 @@ export function registerByKaranteliTools(server, options) {
88
106
  });
89
107
  server.registerTool("get_liquidations", {
90
108
  title: "Crypto liquidations: daily long/short totals per symbol and exchange",
91
- 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.",
109
+ 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, OKX, Gate and HTX 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.",
92
110
  inputSchema: {
93
111
  symbol: z
94
112
  .string()
@@ -107,10 +125,70 @@ export function registerByKaranteliTools(server, options) {
107
125
  const wantDays = days ?? 7;
108
126
  const dates = [...new Set(rows.map((r) => String(r.date)))].sort().reverse().slice(0, wantDays);
109
127
  const dateSet = new Set(dates);
110
- const filtered = rows.filter((r) => dateSet.has(String(r.date)) && (!symbol || String(r.symbol).toUpperCase() === symbol));
128
+ /* Bare "BTC" is accepted like every other tool (audit P3 #414). */
129
+ const wantSymbol = symbol ? (symbol.endsWith("USDT") ? symbol : `${symbol}USDT`) : undefined;
130
+ const filtered = rows.filter((r) => dateSet.has(String(r.date)) && (!wantSymbol || String(r.symbol).toUpperCase() === wantSymbol));
131
+ /* Totals are computed over EVERY matching row before the row cap. A
132
+ * symbol-less 7-day call matches ~7,000 rows; returning only the first
133
+ * 400 (one day, alphabetically) used to understate the weekly total
134
+ * 11.5x with no warning in the payload (audit 2026-08-20 P1-67). */
135
+ const num = (v) => {
136
+ const n = Number(v);
137
+ return Number.isFinite(n) ? n : 0;
138
+ };
139
+ const byDate = new Map();
140
+ let longTotal = 0;
141
+ let shortTotal = 0;
142
+ let eventsTotal = 0;
143
+ for (const r of filtered) {
144
+ const date = String(r.date);
145
+ const long = num(r.long_liquidations_usd);
146
+ const short = num(r.short_liquidations_usd);
147
+ const events = num(r.events);
148
+ longTotal += long;
149
+ shortTotal += short;
150
+ eventsTotal += events;
151
+ const day = byDate.get(date) ?? { date, long_liquidations_usd: 0, short_liquidations_usd: 0, events: 0, rows: 0 };
152
+ day.long_liquidations_usd += long;
153
+ day.short_liquidations_usd += short;
154
+ day.events += events;
155
+ day.rows += 1;
156
+ byDate.set(date, day);
157
+ }
158
+ const MAX_ROWS = 400;
159
+ /* Largest rows first within the cap, newest day first. */
160
+ const ordered = [...filtered].sort((a, b) => {
161
+ const dd = String(b.date).localeCompare(String(a.date));
162
+ if (dd !== 0)
163
+ return dd;
164
+ return (num(b.long_liquidations_usd) + num(b.short_liquidations_usd)) - (num(a.long_liquidations_usd) + num(a.short_liquidations_usd));
165
+ });
166
+ const truncated = ordered.length > MAX_ROWS;
167
+ const baseNote = "Recorded from public exchange streams; totals are a floor (Binance throttles its stream). Live 24h view: " + PUBLIC_URL + "/liquidations";
111
168
  return ok({
112
- rows: filtered.slice(0, 400),
113
- note: "Recorded from public exchange streams; totals are a floor (Binance throttles its stream). Live 24h view: " + PUBLIC_URL + "/liquidations",
169
+ summary: {
170
+ window_days: wantDays,
171
+ dates_covered: dates,
172
+ totals: {
173
+ long_liquidations_usd: Math.round(longTotal),
174
+ short_liquidations_usd: Math.round(shortTotal),
175
+ total_liquidations_usd: Math.round(longTotal + shortTotal),
176
+ events: eventsTotal,
177
+ rows_matched: filtered.length,
178
+ },
179
+ by_date: [...byDate.values()].sort((a, b) => b.date.localeCompare(a.date)).map((d) => ({
180
+ ...d,
181
+ long_liquidations_usd: Math.round(d.long_liquidations_usd),
182
+ short_liquidations_usd: Math.round(d.short_liquidations_usd),
183
+ })),
184
+ },
185
+ rows: ordered.slice(0, MAX_ROWS),
186
+ rows_returned: Math.min(ordered.length, MAX_ROWS),
187
+ rows_matched: filtered.length,
188
+ truncated,
189
+ note: truncated
190
+ ? `rows is capped at ${MAX_ROWS} of ${filtered.length} matching rows (largest first). Use summary.totals and summary.by_date for complete figures, or pass a symbol filter. ` + baseNote
191
+ : baseNote,
114
192
  source: `${PUBLIC_URL}/liquidations`,
115
193
  });
116
194
  }
@@ -142,8 +220,8 @@ export function registerByKaranteliTools(server, options) {
142
220
  }
143
221
  });
144
222
  server.registerTool("get_funding_heatmap", {
145
- title: "Funding rates across top-30 Binance perps",
146
- 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.",
223
+ title: "Funding rates across the ~30 most traded Binance perps",
224
+ description: "Call this when the user asks for the full current funding table across the ~30 most traded Binance perps (28-30 rows; contracts without a live funding print are skipped), 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 most traded Binance USDT-M perpetuals. Positive funding means longs pay shorts.",
147
225
  inputSchema: {
148
226
  symbol: z
149
227
  .string()
@@ -160,7 +238,7 @@ export function registerByKaranteliTools(server, options) {
160
238
  if (!row) {
161
239
  return ok({
162
240
  generatedAt: d.generatedAt,
163
- note: `${want} is not in the top-30 heatmap set. Tracked symbols: ${d.rows.map((r) => r.symbol).join(", ")}`,
241
+ note: `${want} is not in the heatmap set. Tracked symbols: ${d.rows.map((r) => r.symbol).join(", ")}`,
164
242
  });
165
243
  }
166
244
  return ok({ generatedAt: d.generatedAt, row, source: `${PUBLIC_URL}/heatmap` });
@@ -327,7 +405,7 @@ export function registerByKaranteliTools(server, options) {
327
405
  annotations: READ_ONLY,
328
406
  }, async () => {
329
407
  try {
330
- return ok(await fetchJson("/api/public/cot"));
408
+ return ok(withProvenance(await fetchJson("/api/public/cot"), "/api/public/cot"));
331
409
  }
332
410
  catch (err) {
333
411
  return fail(err);
@@ -340,7 +418,7 @@ export function registerByKaranteliTools(server, options) {
340
418
  annotations: READ_ONLY,
341
419
  }, async () => {
342
420
  try {
343
- return ok(await fetchJson("/api/public/options"));
421
+ return ok(withProvenance(await fetchJson("/api/public/options"), "/api/public/options"));
344
422
  }
345
423
  catch (err) {
346
424
  return fail(err);
@@ -353,7 +431,7 @@ export function registerByKaranteliTools(server, options) {
353
431
  annotations: READ_ONLY,
354
432
  }, async () => {
355
433
  try {
356
- return ok(await fetchJson("/api/public/premium"));
434
+ return ok(withProvenance(await fetchJson("/api/public/premium"), "/api/public/premium"));
357
435
  }
358
436
  catch (err) {
359
437
  return fail(err);
@@ -366,7 +444,7 @@ export function registerByKaranteliTools(server, options) {
366
444
  annotations: READ_ONLY,
367
445
  }, async () => {
368
446
  try {
369
- return ok(await fetchJson("/api/public/flow"));
447
+ return ok(withProvenance(await fetchJson("/api/public/flow"), "/api/public/flow"));
370
448
  }
371
449
  catch (err) {
372
450
  return fail(err);
@@ -379,7 +457,7 @@ export function registerByKaranteliTools(server, options) {
379
457
  annotations: READ_ONLY,
380
458
  }, async () => {
381
459
  try {
382
- return ok(await fetchJson("/api/public/options-flow"));
460
+ return ok(withProvenance(await fetchJson("/api/public/options-flow"), "/api/public/options-flow"));
383
461
  }
384
462
  catch (err) {
385
463
  return fail(err);
@@ -392,7 +470,7 @@ export function registerByKaranteliTools(server, options) {
392
470
  annotations: READ_ONLY,
393
471
  }, async () => {
394
472
  try {
395
- return ok(await fetchJson("/api/public/slippage"));
473
+ return ok(withProvenance(await fetchJson("/api/public/slippage"), "/api/public/slippage"));
396
474
  }
397
475
  catch (err) {
398
476
  return fail(err);
@@ -405,7 +483,7 @@ export function registerByKaranteliTools(server, options) {
405
483
  annotations: READ_ONLY,
406
484
  }, async () => {
407
485
  try {
408
- return ok(await fetchJson("/api/public/events"));
486
+ return ok(withProvenance(await fetchJson("/api/public/events"), "/api/public/events"));
409
487
  }
410
488
  catch (err) {
411
489
  return fail(err);
@@ -418,7 +496,7 @@ export function registerByKaranteliTools(server, options) {
418
496
  annotations: READ_ONLY,
419
497
  }, async () => {
420
498
  try {
421
- return ok(await fetchJson("/api/public/incidents"));
499
+ return ok(withProvenance(await fetchJson("/api/public/incidents"), "/api/public/incidents"));
422
500
  }
423
501
  catch (err) {
424
502
  return fail(err);
@@ -431,7 +509,7 @@ export function registerByKaranteliTools(server, options) {
431
509
  annotations: READ_ONLY,
432
510
  }, async () => {
433
511
  try {
434
- return ok(await fetchJson("/api/public/oi"));
512
+ return ok(withProvenance(await fetchJson("/api/public/oi"), "/api/public/oi"));
435
513
  }
436
514
  catch (err) {
437
515
  return fail(err);
@@ -444,7 +522,187 @@ export function registerByKaranteliTools(server, options) {
444
522
  annotations: READ_ONLY,
445
523
  }, async () => {
446
524
  try {
447
- return ok(await fetchJson("/api/public/charge"));
525
+ return ok(withProvenance(await fetchJson("/api/public/charge"), "/api/public/charge"));
526
+ }
527
+ catch (err) {
528
+ return fail(err);
529
+ }
530
+ });
531
+ server.registerTool("get_altseason", {
532
+ title: "Altcoin Season Index (live + recorded history)",
533
+ description: "Call this when the user asks whether it is altseason, how altcoins are doing against Bitcoin, or about market rotation. Returns the live Altcoin Season Index (share of the top 50 Binance perpetual altcoins beating BTC over the trailing 90 days; >=75 altseason, <=25 bitcoin season), the strongest and weakest large alts, and the recorded daily history (never reconstructed).",
534
+ inputSchema: {},
535
+ annotations: READ_ONLY,
536
+ }, async () => {
537
+ try {
538
+ return ok(withProvenance(await fetchJson("/api/public/altseason"), "/api/public/altseason"));
539
+ }
540
+ catch (err) {
541
+ return fail(err);
542
+ }
543
+ });
544
+ server.registerTool("get_quantum_exposure", {
545
+ title: "Quantum-exposed Bitcoin (daily first-party measurement)",
546
+ description: "Call this when the user asks how much Bitcoin is vulnerable to a quantum computer, about quantum-exposed supply, P2PK coins, or Satoshi-era exposure. Returns the latest daily measurement from ByKaranteli's own Bitcoin Core node: exposed BTC and its share of held value and UTXO count, composition by script family, dormancy cohorts, the dormant-P2PK watch set, and provenance hashes (base_height, base_hash, txoutset_hash) so any figure can be re-verified against any node.",
547
+ inputSchema: {},
548
+ annotations: READ_ONLY,
549
+ }, async () => {
550
+ try {
551
+ return ok(withProvenance(await fetchJson("/api/public/quantum"), "/api/public/quantum"));
552
+ }
553
+ catch (err) {
554
+ return fail(err);
555
+ }
556
+ });
557
+ server.registerTool("get_metric_context", {
558
+ title: "Historical context for any recorded metric (conditional distribution)",
559
+ description: "Call this when the user asks whether a metric's current reading is high or low, or what happened after similar readings. Buckets today's value against the metric's own recorded daily history and returns the median forward BTC return and up-share per bucket at +1/+3/+7 days, with the all-days base rate alongside. Honesty rules: buckets under 30 days are suppressed, and most metrics do NOT separate from the base rate; the interpretation says so plainly. History, not a forecast. Metrics include coinbase_premium_pct, kraken_btc_premium_pct, dvol_btc, fear_greed, funding_btc_daily_pct, etf_btc_net_flow_usd, vpin_btc, altseason_index, stablecoin_total_mcap_busd, fred_dff, fred_dgs10, fred_walcl_busd, fred_rrp_busd and the btc_* network series.",
560
+ inputSchema: {
561
+ metric: z
562
+ .string()
563
+ .regex(/^[a-z0-9_]{2,50}$/)
564
+ .describe("Metric key, e.g. coinbase_premium_pct, fear_greed, altseason_index, stablecoin_total_mcap_busd."),
565
+ },
566
+ annotations: READ_ONLY,
567
+ }, async ({ metric }) => {
568
+ try {
569
+ return ok(withProvenance(await fetchJson(`/api/public/context?metric=${encodeURIComponent(metric)}`), `/api/public/context?metric=${encodeURIComponent(metric)}`));
570
+ }
571
+ catch (err) {
572
+ return fail(err);
573
+ }
574
+ });
575
+ server.registerTool("get_theme_indices", {
576
+ title: "Crypto narrative indices (AI, RWA, DePIN, meme, L1, L2, DeFi, quantum)",
577
+ description: "Call this when the user asks which crypto narrative or sector is leading, about rotation between AI, RWA, DePIN, memecoins, layer 1, layer 2, DeFi or quantum coins, or for a theme index. Returns eight equal-weight fixed-basket indices rebased to 100 on 2025-01-01 with 1d/7d/30d/90d/YTD returns, vs BTC, and the member lists; daily points are omitted unless include_points is true.",
578
+ inputSchema: { include_points: z.boolean().optional().describe("boolean, optional: include the daily index points (large)") },
579
+ annotations: READ_ONLY,
580
+ }, async (args) => {
581
+ try {
582
+ const path = args.include_points === true ? "/api/public/themes" : "/api/public/themes?points=0";
583
+ return ok(withProvenance(await fetchJson(path), path));
584
+ }
585
+ catch (err) {
586
+ return fail(err);
587
+ }
588
+ });
589
+ server.registerTool("get_factor_board", {
590
+ title: "Factor board: what followed days like today across recorded metrics",
591
+ description: "Call this when the user asks which indicators currently sit in an unusual band, whether a metric's current level historically preceded BTC moves, or for a cross-metric conditional overview. Returns every recorded metric in its historical band with the median 7-day BTC move that followed versus the base rate, with an n >= 30 gate; distributions, not forecasts.",
592
+ inputSchema: {},
593
+ annotations: READ_ONLY,
594
+ }, async () => {
595
+ try {
596
+ return ok(withProvenance(await fetchJson("/api/public/factors"), "/api/public/factors"));
597
+ }
598
+ catch (err) {
599
+ return fail(err);
600
+ }
601
+ });
602
+ server.registerTool("get_venue_markets", {
603
+ title: "Exchange coverage: OI, volume, funding and pegs across 43 exchanges",
604
+ description: "Call this when the user asks about total open interest across exchanges, which venues hold the most OI, DEX versus CEX share, funding dispersion between venues, or stablecoin pegs. Returns the latest 10-minute snapshot aggregates across 56 feeds on 43 exchanges; pass symbol for one coin's per-venue rows.",
605
+ inputSchema: { symbol: z.string().optional().describe("string, optional base asset, e.g. BTC") },
606
+ annotations: READ_ONLY,
607
+ }, async (args) => {
608
+ try {
609
+ const path = args.symbol ? `/api/public/venues/markets?symbol=${encodeURIComponent(args.symbol.toUpperCase())}` : "/api/public/venues/markets";
610
+ return ok(withProvenance(await fetchJson(path), path));
611
+ }
612
+ catch (err) {
613
+ return fail(err);
614
+ }
615
+ });
616
+ server.registerTool("get_lead_lag", {
617
+ title: "Venue lead-lag: who moves first (Coinbase, Kraken, Binance)",
618
+ description: "Call this when the user asks which exchange leads price discovery or whether spot or perp moves first. Returns per-pair daily cross-correlations of one-minute returns at lags -3..+3 and the lead asymmetry, with the share of days each venue led.",
619
+ inputSchema: {},
620
+ annotations: READ_ONLY,
621
+ }, async () => {
622
+ try {
623
+ return ok(withProvenance(await fetchJson("/api/public/venues/lead-lag"), "/api/public/venues/lead-lag"));
624
+ }
625
+ catch (err) {
626
+ return fail(err);
627
+ }
628
+ });
629
+ server.registerTool("get_iv_surface", {
630
+ title: "Options implied-volatility surface and 25-delta skew",
631
+ description: "Call this when the user asks about implied volatility by strike or expiry, skew, put versus call IV, term structure of IV, or whether downside protection is expensive. Returns the IV surface (expiry x moneyness), per-expiry ATM / 25-delta put and call IV, skew and butterfly, and the constant-30d history, from the daily Deribit chain.",
632
+ inputSchema: { currency: z.string().optional().describe("BTC or ETH, default BTC") },
633
+ annotations: READ_ONLY,
634
+ }, async (args) => {
635
+ try {
636
+ const path = `/api/public/options/surface?currency=${(args.currency ?? "BTC").toUpperCase() === "ETH" ? "ETH" : "BTC"}`;
637
+ return ok(withProvenance(await fetchJson(path), path));
638
+ }
639
+ catch (err) {
640
+ return fail(err);
641
+ }
642
+ });
643
+ server.registerTool("get_whale_tape", {
644
+ title: "Whale tape: $1M+ aggressive prints with 24h buy share",
645
+ description: "Call this when the user asks about whale trades, large market orders, or whether big players are buying or selling right now. Returns recent $1M+ aggressive prints recorded live from our own sockets and 24h aggregates with the buy share.",
646
+ inputSchema: {},
647
+ annotations: READ_ONLY,
648
+ }, async () => {
649
+ try {
650
+ return ok(withProvenance(await fetchJson("/api/public/whales"), "/api/public/whales"));
651
+ }
652
+ catch (err) {
653
+ return fail(err);
654
+ }
655
+ });
656
+ server.registerTool("get_correlations", {
657
+ title: "Crypto correlation matrix",
658
+ description: "Call this when the user asks how correlated two coins are, for decorrelated pairs, or how tightly alts track BTC. Returns the 30-day rolling Pearson correlation matrix of daily returns across the top perpetuals.",
659
+ inputSchema: {},
660
+ annotations: READ_ONLY,
661
+ }, async () => {
662
+ try {
663
+ return ok(withProvenance(await fetchJson("/api/public/correlations"), "/api/public/correlations"));
664
+ }
665
+ catch (err) {
666
+ return fail(err);
667
+ }
668
+ });
669
+ server.registerTool("get_new_listings", {
670
+ title: "New and delisted perpetual contracts",
671
+ description: "Call this when the user asks what new perpetuals were listed, which exchange listed a coin first, or about delistings. Returns listings and delistings across six exchanges from the hourly scan.",
672
+ inputSchema: { days: z.number().optional().describe("number, optional window in days, default 30") },
673
+ annotations: READ_ONLY,
674
+ }, async (args) => {
675
+ try {
676
+ const path = `/api/public/listings?days=${args.days && args.days > 0 ? Math.floor(args.days) : 30}`;
677
+ return ok(withProvenance(await fetchJson(path), path));
678
+ }
679
+ catch (err) {
680
+ return fail(err);
681
+ }
682
+ });
683
+ server.registerTool("get_macro_liquidity", {
684
+ title: "Macro liquidity: Fed funds, 10y, balance sheet, RRP, stablecoin supply",
685
+ description: "Call this when the user asks about macro liquidity, the Fed balance sheet, reverse repo, rates or stablecoin supply in relation to crypto. Returns the recorded daily series and latest values.",
686
+ inputSchema: { days: z.number().optional().describe("number, optional window in days, default 365") },
687
+ annotations: READ_ONLY,
688
+ }, async (args) => {
689
+ try {
690
+ const path = `/api/public/macro?days=${args.days && args.days > 0 ? Math.floor(args.days) : 365}`;
691
+ return ok(withProvenance(await fetchJson(path), path));
692
+ }
693
+ catch (err) {
694
+ return fail(err);
695
+ }
696
+ });
697
+ server.registerTool("get_network_health", {
698
+ title: "Bitcoin network health from our own node",
699
+ description: "Call this when the user asks about Bitcoin hashrate, difficulty, fees or mempool congestion. Returns the recorded daily series and latest values measured on ByKaranteli's own node.",
700
+ inputSchema: { days: z.number().optional().describe("number, optional window in days, default 365") },
701
+ annotations: READ_ONLY,
702
+ }, async (args) => {
703
+ try {
704
+ const path = `/api/public/network?days=${args.days && args.days > 0 ? Math.floor(args.days) : 365}`;
705
+ return ok(withProvenance(await fetchJson(path), path));
448
706
  }
449
707
  catch (err) {
450
708
  return fail(err);
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "bykaranteli-mcp",
3
- "version": "0.7.3",
3
+ "version": "0.9.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": {
7
7
  "bykaranteli-mcp": "dist/index.js"
8
8
  },
9
- "main": "dist/index.js",
9
+ "main": "dist/tools.js",
10
10
  "files": [
11
11
  "dist",
12
12
  "README.md",