@finchagentic/mcp 4.6.1 → 4.6.2

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 (58) hide show
  1. package/package.json +5 -6
  2. package/dist/_http-cache.js +0 -96
  3. package/dist/_text-search.js +0 -39
  4. package/dist/agent-loop.js +0 -301
  5. package/dist/annotations.js +0 -122
  6. package/dist/cli.js +0 -1391
  7. package/dist/clink-input.js +0 -15
  8. package/dist/config.js +0 -132
  9. package/dist/convex.js +0 -175
  10. package/dist/dex-pair.js +0 -54
  11. package/dist/enrichment-router.js +0 -315
  12. package/dist/index.js +0 -258
  13. package/dist/llm.js +0 -298
  14. package/dist/local-memory-file.js +0 -150
  15. package/dist/local-memory.js +0 -135
  16. package/dist/local-vault.js +0 -456
  17. package/dist/output-schemas.js +0 -605
  18. package/dist/project.js +0 -36
  19. package/dist/prompts.js +0 -111
  20. package/dist/public-url.js +0 -107
  21. package/dist/resources.js +0 -111
  22. package/dist/server.js +0 -322
  23. package/dist/signal-gate.js +0 -57
  24. package/dist/token-decimals.js +0 -26
  25. package/dist/token-gate.js +0 -88
  26. package/dist/tool-filter.js +0 -53
  27. package/dist/tools/_solidity-scan.js +0 -313
  28. package/dist/tools/agents.js +0 -441
  29. package/dist/tools/automation.js +0 -354
  30. package/dist/tools/base-mcp.js +0 -466
  31. package/dist/tools/base.js +0 -283
  32. package/dist/tools/chronicle.js +0 -268
  33. package/dist/tools/coder.js +0 -94
  34. package/dist/tools/deep-research.js +0 -1421
  35. package/dist/tools/defi.js +0 -292
  36. package/dist/tools/equity.js +0 -372
  37. package/dist/tools/events.js +0 -182
  38. package/dist/tools/github.js +0 -564
  39. package/dist/tools/insider.js +0 -264
  40. package/dist/tools/insight.js +0 -630
  41. package/dist/tools/market.js +0 -555
  42. package/dist/tools/memory.js +0 -1059
  43. package/dist/tools/miroshark.js +0 -350
  44. package/dist/tools/monitor.js +0 -319
  45. package/dist/tools/os.js +0 -236
  46. package/dist/tools/packets.js +0 -296
  47. package/dist/tools/research-chain.js +0 -226
  48. package/dist/tools/research-compare.js +0 -280
  49. package/dist/tools/research.js +0 -188
  50. package/dist/tools/rh-bridge.js +0 -148
  51. package/dist/tools/rh-mcp.js +0 -1448
  52. package/dist/tools/rh-orders.js +0 -556
  53. package/dist/tools/scanner.js +0 -564
  54. package/dist/tools/stake.js +0 -369
  55. package/dist/tools/vault.js +0 -1020
  56. package/dist/tools/wallet.js +0 -200
  57. package/dist/types.js +0 -2
  58. package/dist/wallet.js +0 -372
@@ -1,315 +0,0 @@
1
- "use strict";
2
- // Generalized enrichment router for deep_research synthesis.
3
- //
4
- // Old behavior: Firecrawl scraped 5–10 pages and the LLM synthesized. When
5
- // pages were thin or outdated, the LLM filled gaps with plausible fiction.
6
- //
7
- // New behavior: detect the topic domain and hit primary-source APIs in
8
- // parallel before synthesis. Inject results as "AUTHORITATIVE LIVE DATA"
9
- // the LLM is instructed to lead with.
10
- //
11
- // All APIs used here are free, public, no auth required:
12
- // - DefiLlama, CoinGecko (crypto)
13
- // - HackerNews Algolia (tech news, ~real-time)
14
- // - GitHub search (repos, no auth needed for public read)
15
- // - arXiv (academic papers)
16
- // - Wikipedia REST (foundational facts)
17
- //
18
- // All calls are best-effort with short timeouts - if any fail, the
19
- // synthesis runs without that block.
20
- Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.enrichQuery = enrichQuery;
22
- exports.todayContext = todayContext;
23
- // ─── Topic detection ──────────────────────────────────────────────────────────
24
- const CRYPTO_RE = /\b(tvl|apy|yield|vault|defi|stablecoin|usdc|usdt|eth|btc|sol|bitcoin|ethereum|solana|base|aerodrome|uniswap|morpho|moonwell|aave|lido|ethena|pendle|curve|gauntlet|steakhouse|base chain|arbitrum|optimism|polygon|onchain|on-chain|wallet|swap|amm|perpetual|perp|liquid staking)\b/i;
25
- const TECH_RE = /\b(ai|llm|gpt|claude|anthropic|openai|gemini|machine learning|deep learning|neural|transformer|frontier model|agent|mcp|api|framework|library|sdk|github|repo|repository|startup|y combinator|yc|saas|vc|funding round|seed|series [a-d]|launches|launched|release|shipped|build|launch)\b/i;
26
- const ACADEMIC_RE = /\b(paper|research paper|arxiv|study|publication|preprint|peer.review|citation|abstract|methodology|hypothesis|empirical|experiment|finding[s]?|literature review|systematic review|meta.analysis|theorem|proof)\b/i;
27
- const FACTUAL_RE = /\b(history of|founded|established|definition|what is|who is|when did|where is|biography|encyclopedia|background|origin|first invented)\b/i;
28
- function detectDomains(query) {
29
- const q = query.toLowerCase();
30
- const domains = [];
31
- if (CRYPTO_RE.test(q))
32
- domains.push("crypto");
33
- if (TECH_RE.test(q))
34
- domains.push("tech");
35
- if (ACADEMIC_RE.test(q))
36
- domains.push("academic");
37
- if (FACTUAL_RE.test(q))
38
- domains.push("general");
39
- return domains;
40
- }
41
- // ─── Formatting helpers ──────────────────────────────────────────────────────
42
- function formatUsd(n) {
43
- if (typeof n !== "number" || !isFinite(n))
44
- return "n/a";
45
- if (n >= 1e9)
46
- return `$${(n / 1e9).toFixed(2)}B`;
47
- if (n >= 1e6)
48
- return `$${(n / 1e6).toFixed(2)}M`;
49
- if (n >= 1e3)
50
- return `$${(n / 1e3).toFixed(2)}K`;
51
- return `$${n.toFixed(2)}`;
52
- }
53
- function clipText(s, n) {
54
- return s.length > n ? s.slice(0, n - 1) + "…" : s;
55
- }
56
- function ageInDays(iso) {
57
- return Math.floor((Date.now() - new Date(iso).getTime()) / 86400000);
58
- }
59
- // ─── Crypto enrichment (DefiLlama + CoinGecko) ────────────────────────────────
60
- const DEFI_PROTOCOLS = [
61
- "aerodrome", "uniswap", "morpho", "moonwell", "aave", "compound", "lido",
62
- "rocket pool", "rocketpool", "ethena", "pendle", "curve", "balancer",
63
- "convex", "frax", "spark", "fluid", "kamino", "marginfi", "jupiter",
64
- "raydium", "orca", "drift", "gmx", "synthetix", "dydx", "vertex",
65
- "hyperliquid", "yearn", "gauntlet", "steakhouse",
66
- ];
67
- const CHAIN_KEYWORDS = [
68
- "ethereum", "base", "arbitrum", "optimism", "polygon", "solana", "avalanche",
69
- "blast", "sonic", "linea", "scroll", "zksync", "berachain", "monad",
70
- ];
71
- const TOP_TOKENS = [
72
- "btc", "bitcoin", "eth", "ethereum", "usdc", "usdt", "dai", "weth",
73
- "sol", "solana", "matic", "avax", "link", "uni", "aave", "ldo",
74
- ];
75
- async function cryptoEnrich(query) {
76
- const q = query.toLowerCase();
77
- const protocols = DEFI_PROTOCOLS.filter((p) => q.includes(p));
78
- const chains = CHAIN_KEYWORDS.filter((c) => q.includes(c));
79
- const tokens = TOP_TOKENS.filter((t) => new RegExp(`\\b${t}\\b`, "i").test(q));
80
- const blocks = [];
81
- // Protocol TVL
82
- if (protocols.length > 0) {
83
- try {
84
- const slug = protocols[0].toLowerCase().replace(/\s+/g, "-");
85
- const res = await fetch(`https://api.llama.fi/protocol/${slug}`, { signal: AbortSignal.timeout(8000) });
86
- if (res.ok) {
87
- const d = (await res.json());
88
- if (d?.name) {
89
- const chainBreakdown = d.currentChainTvls
90
- ? Object.entries(d.currentChainTvls)
91
- .sort(([, a], [, b]) => b - a)
92
- .slice(0, 5)
93
- .map(([c, v]) => `${c}: ${formatUsd(v)}`)
94
- .join(" · ")
95
- : "";
96
- blocks.push([
97
- `### 📊 DefiLlama - ${d.name}`,
98
- `- TVL: **${formatUsd(d.tvl)}** | 24h: ${d.change_1d?.toFixed(2) ?? "n/a"}% | 7d: ${d.change_7d?.toFixed(2) ?? "n/a"}%`,
99
- d.mcap ? `- Market cap: ${formatUsd(d.mcap)}` : "",
100
- chainBreakdown ? `- Top chains: ${chainBreakdown}` : "",
101
- `- Source: https://defillama.com/protocol/${slug}`,
102
- ].filter(Boolean).join("\n"));
103
- }
104
- }
105
- }
106
- catch { /* ignore */ }
107
- }
108
- // Yields
109
- if (q.match(/\b(yield|vault|apy|lp)\b/)) {
110
- try {
111
- const res = await fetch("https://yields.llama.fi/pools", { signal: AbortSignal.timeout(8000) });
112
- if (res.ok) {
113
- const d = (await res.json());
114
- let pools = (d?.data ?? []);
115
- if (chains[0])
116
- pools = pools.filter((p) => (p.chain ?? "").toLowerCase() === chains[0]);
117
- if (tokens[0])
118
- pools = pools.filter((p) => (p.symbol ?? "").toLowerCase().includes(tokens[0]));
119
- pools = pools.filter((p) => (p.tvlUsd ?? 0) > 1000000 && (p.apy ?? 0) < 100);
120
- pools = pools.sort((a, b) => (b.tvlUsd ?? 0) - (a.tvlUsd ?? 0)).slice(0, 8);
121
- if (pools.length > 0) {
122
- const rows = pools.map((p) => `- **${p.project}** ${p.symbol} on ${p.chain}: ${(p.apy ?? 0).toFixed(2)}% APY · TVL ${formatUsd(p.tvlUsd)}`).join("\n");
123
- blocks.push(`### 💰 DefiLlama Yields (TVL > $1M)\n${rows}\n- Source: https://yields.llama.fi/`);
124
- }
125
- }
126
- }
127
- catch { /* ignore */ }
128
- }
129
- // Prices
130
- if (tokens.length > 0) {
131
- try {
132
- const idMap = {
133
- btc: "bitcoin", eth: "ethereum", sol: "solana", matic: "matic-network",
134
- usdc: "usd-coin", usdt: "tether", dai: "dai", weth: "weth",
135
- avax: "avalanche-2", link: "chainlink", uni: "uniswap", aave: "aave",
136
- ldo: "lido-dao",
137
- };
138
- const ids = tokens.map((t) => idMap[t.toLowerCase()] ?? t.toLowerCase()).join(",");
139
- const res = await fetch(`https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd&include_24hr_change=true&include_market_cap=true`, { signal: AbortSignal.timeout(8000) });
140
- if (res.ok) {
141
- const d = (await res.json());
142
- const rows = Object.entries(d).map(([id, p]) => {
143
- const change = typeof p.usd_24h_change === "number"
144
- ? ` (${p.usd_24h_change >= 0 ? "+" : ""}${p.usd_24h_change.toFixed(2)}% 24h)`
145
- : "";
146
- const mcap = p.usd_market_cap ? ` · mcap ${formatUsd(p.usd_market_cap)}` : "";
147
- return `- **${id}**: $${(p.usd ?? 0).toLocaleString()}${change}${mcap}`;
148
- }).join("\n");
149
- if (rows)
150
- blocks.push(`### 💵 CoinGecko\n${rows}\n- Source: https://www.coingecko.com/`);
151
- }
152
- }
153
- catch { /* ignore */ }
154
- }
155
- return blocks.length > 0 ? blocks.join("\n\n") : null;
156
- }
157
- // ─── Tech enrichment (HackerNews Algolia + GitHub search) ────────────────────
158
- async function techEnrich(query) {
159
- const blocks = [];
160
- // HackerNews - last 30 days, sorted by relevance + popularity
161
- try {
162
- const since = Math.floor((Date.now() - 30 * 86400000) / 1000);
163
- const url = `https://hn.algolia.com/api/v1/search?query=${encodeURIComponent(query)}&numericFilters=created_at_i>${since}&hitsPerPage=8`;
164
- const res = await fetch(url, { signal: AbortSignal.timeout(8000) });
165
- if (res.ok) {
166
- const d = (await res.json());
167
- const hits = (d?.hits ?? []);
168
- if (hits.length > 0) {
169
- const rows = hits.slice(0, 6).map((h) => {
170
- const title = h.title ?? h.story_title ?? "(no title)";
171
- const points = h.points != null ? `${h.points} pts` : "";
172
- const comments = h.num_comments != null ? `${h.num_comments} comments` : "";
173
- const age = h.created_at ? `${ageInDays(h.created_at)}d ago` : "";
174
- const meta = [points, comments, age].filter(Boolean).join(" · ");
175
- const link = h.url ?? `https://news.ycombinator.com/item?id=${h.objectID}`;
176
- return `- **${clipText(title, 140)}** · ${meta}\n ${link}`;
177
- }).join("\n");
178
- blocks.push(`### 📰 HackerNews (last 30d, top hits)\n${rows}\n- Source: https://hn.algolia.com/`);
179
- }
180
- }
181
- }
182
- catch { /* ignore */ }
183
- // GitHub search - public repos matching query
184
- try {
185
- const url = `https://api.github.com/search/repositories?q=${encodeURIComponent(query)}&sort=stars&order=desc&per_page=6`;
186
- const res = await fetch(url, {
187
- signal: AbortSignal.timeout(8000),
188
- headers: { Accept: "application/vnd.github+json", "User-Agent": "finch-mcp" },
189
- });
190
- if (res.ok) {
191
- const d = (await res.json());
192
- const repos = (d?.items ?? []);
193
- if (repos.length > 0) {
194
- const rows = repos.slice(0, 5).map((r) => {
195
- const stars = r.stargazers_count?.toLocaleString() ?? "?";
196
- const desc = clipText(r.description ?? "", 100);
197
- return `- **${r.full_name}** (★${stars}) - ${desc}\n ${r.html_url}`;
198
- }).join("\n");
199
- blocks.push(`### 🐙 GitHub (top repos by stars)\n${rows}\n- Source: https://github.com/search?q=${encodeURIComponent(query)}`);
200
- }
201
- }
202
- }
203
- catch { /* ignore */ }
204
- return blocks.length > 0 ? blocks.join("\n\n") : null;
205
- }
206
- // ─── Academic enrichment (arXiv) ─────────────────────────────────────────────
207
- async function academicEnrich(query) {
208
- try {
209
- const url = `http://export.arxiv.org/api/query?search_query=all:${encodeURIComponent(query)}&start=0&max_results=5&sortBy=submittedDate&sortOrder=descending`;
210
- const res = await fetch(url, { signal: AbortSignal.timeout(8000) });
211
- if (!res.ok)
212
- return null;
213
- const xml = await res.text();
214
- const entries = xml.match(/<entry>[\s\S]*?<\/entry>/g) ?? [];
215
- if (entries.length === 0)
216
- return null;
217
- const rows = entries.slice(0, 5).map((e) => {
218
- const title = (e.match(/<title>([\s\S]*?)<\/title>/)?.[1] ?? "").replace(/\s+/g, " ").trim();
219
- const summary = (e.match(/<summary>([\s\S]*?)<\/summary>/)?.[1] ?? "").replace(/\s+/g, " ").trim();
220
- const published = e.match(/<published>([\s\S]*?)<\/published>/)?.[1] ?? "";
221
- const link = e.match(/<id>([\s\S]*?)<\/id>/)?.[1] ?? "";
222
- const date = published ? `${published.slice(0, 10)} (${ageInDays(published)}d ago)` : "";
223
- return `- **${clipText(title, 140)}** · ${date}\n ${clipText(summary, 200)}\n ${link}`;
224
- }).join("\n\n");
225
- return `### 📚 arXiv (recent papers)\n${rows}\n- Source: https://arxiv.org/`;
226
- }
227
- catch {
228
- return null;
229
- }
230
- }
231
- // ─── General enrichment (Wikipedia REST) ─────────────────────────────────────
232
- async function generalEnrich(query) {
233
- try {
234
- // Wikipedia search - find best matching article
235
- const searchRes = await fetch(`https://en.wikipedia.org/w/api.php?action=opensearch&format=json&search=${encodeURIComponent(query)}&limit=3`, { signal: AbortSignal.timeout(6000) });
236
- if (!searchRes.ok)
237
- return null;
238
- const [, titles, descs, urls] = (await searchRes.json());
239
- if (!titles || titles.length === 0)
240
- return null;
241
- // Fetch summary for top hit
242
- const top = titles[0];
243
- const sumRes = await fetch(`https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(top)}`, { signal: AbortSignal.timeout(6000) });
244
- if (!sumRes.ok)
245
- return null;
246
- const summary = (await sumRes.json());
247
- const lines = [
248
- `### 🌐 Wikipedia - ${summary.title ?? top}`,
249
- summary.extract ? clipText(summary.extract, 600) : descs[0] ?? "",
250
- `- Source: ${urls[0] ?? `https://en.wikipedia.org/wiki/${encodeURIComponent(top)}`}`,
251
- ];
252
- return lines.filter(Boolean).join("\n");
253
- }
254
- catch {
255
- return null;
256
- }
257
- }
258
- // ─── Public entry point ──────────────────────────────────────────────────────
259
- /**
260
- * Run enrichment in parallel for all detected domains. Pure best-effort: if
261
- * no domain matches or all APIs fail, returns hasData=false and synthesis
262
- * runs normally.
263
- */
264
- async function enrichQuery(query) {
265
- const domains = detectDomains(query);
266
- if (domains.length === 0) {
267
- return { context: "", authoritative: [], hasData: false, domains: [] };
268
- }
269
- const promises = [];
270
- if (domains.includes("crypto"))
271
- promises.push(cryptoEnrich(query));
272
- if (domains.includes("tech"))
273
- promises.push(techEnrich(query));
274
- if (domains.includes("academic"))
275
- promises.push(academicEnrich(query));
276
- if (domains.includes("general"))
277
- promises.push(generalEnrich(query));
278
- const results = await Promise.all(promises);
279
- const blocks = results.filter((b) => !!b);
280
- if (blocks.length === 0) {
281
- return { context: "", authoritative: [], hasData: false, domains };
282
- }
283
- const authoritative = [];
284
- if (domains.includes("crypto"))
285
- authoritative.push("defillama.com", "coingecko.com");
286
- if (domains.includes("tech"))
287
- authoritative.push("news.ycombinator.com", "github.com");
288
- if (domains.includes("academic"))
289
- authoritative.push("arxiv.org");
290
- if (domains.includes("general"))
291
- authoritative.push("wikipedia.org");
292
- const todayISO = new Date().toISOString().slice(0, 10);
293
- const context = [
294
- `---`,
295
- `## 🔒 AUTHORITATIVE LIVE DATA - fetched ${todayISO}`,
296
- ``,
297
- `The blocks below come from primary-source APIs called at query time. Treat as ground truth. When figures here conflict with the scraped sources below, prefer these and cite them by source name (e.g. \`[DefiLlama]\`, \`[HackerNews]\`, \`[arXiv]\`, \`[GitHub]\`, \`[Wikipedia]\`).`,
298
- ``,
299
- blocks.join("\n\n"),
300
- `---`,
301
- ].join("\n");
302
- return { context, authoritative, hasData: true, domains };
303
- }
304
- /**
305
- * Returns the current date in ISO format. Used by the synthesis prompt to
306
- * anchor the LLM in time - prevents "Q2 2026 catalysts in July" style
307
- * calendar hallucinations.
308
- */
309
- function todayContext() {
310
- const now = new Date();
311
- const iso = now.toISOString().slice(0, 10);
312
- const weekday = now.toLocaleDateString("en-US", { weekday: "long" });
313
- const quarter = `Q${Math.floor(now.getUTCMonth() / 3) + 1}`;
314
- return `Today is ${weekday}, ${iso}. This is ${quarter} ${now.getUTCFullYear()}. Treat any date after today as a future/projected event, not a confirmed one.`;
315
- }
package/dist/index.js DELETED
@@ -1,258 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
- if (k2 === undefined) k2 = k;
5
- var desc = Object.getOwnPropertyDescriptor(m, k);
6
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
- desc = { enumerable: true, get: function() { return m[k]; } };
8
- }
9
- Object.defineProperty(o, k2, desc);
10
- }) : (function(o, m, k, k2) {
11
- if (k2 === undefined) k2 = k;
12
- o[k2] = m[k];
13
- }));
14
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
- Object.defineProperty(o, "default", { enumerable: true, value: v });
16
- }) : function(o, v) {
17
- o["default"] = v;
18
- });
19
- var __importStar = (this && this.__importStar) || (function () {
20
- var ownKeys = function(o) {
21
- ownKeys = Object.getOwnPropertyNames || function (o) {
22
- var ar = [];
23
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
- return ar;
25
- };
26
- return ownKeys(o);
27
- };
28
- return function (mod) {
29
- if (mod && mod.__esModule) return mod;
30
- var result = {};
31
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
- __setModuleDefault(result, mod);
33
- return result;
34
- };
35
- })();
36
- Object.defineProperty(exports, "__esModule", { value: true });
37
- const server_js_1 = require("./server.js");
38
- const tool_filter_js_1 = require("./tool-filter.js");
39
- const wallet_js_1 = require("./wallet.js");
40
- const config_js_1 = require("./config.js");
41
- const clink_input_js_1 = require("./clink-input.js");
42
- const readline = __importStar(require("readline"));
43
- const fs = __importStar(require("fs"));
44
- const path = __importStar(require("path"));
45
- // Always read the version from package.json so banner + boot strings stay in
46
- // sync after every npm publish - no more hand-edits in three places.
47
- const PKG_VERSION = (() => {
48
- try {
49
- // dist/index.js -> ../package.json (CJS so __dirname is available)
50
- const raw = fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8");
51
- return JSON.parse(raw).version ?? "unknown";
52
- }
53
- catch {
54
- return "unknown";
55
- }
56
- })();
57
- const CONVEX_SITE = process.env.FINCH_CONVEX_URL ?? "https://befitting-porcupine-276.convex.site";
58
- // ── ANSI helpers ──────────────────────────────────────────────────────────────
59
- const C = {
60
- cyan: "\x1b[36m",
61
- dim: "\x1b[90m",
62
- white: "\x1b[97m",
63
- green: "\x1b[32m",
64
- yellow: "\x1b[33m",
65
- red: "\x1b[31m",
66
- reset: "\x1b[0m",
67
- bold: "\x1b[1m",
68
- };
69
- // Was still spelling out the pre-rebrand "NOELCLAW" wordmark in ASCII art -
70
- // leftover from before the Finch rebrand, never caught because nothing
71
- // visually diffs a banner string.
72
- const BANNER = `
73
- ${C.cyan}
74
- ███████╗██╗███╗ ██╗ ██████╗██╗ ██╗
75
- ██╔════╝██║████╗ ██║██╔════╝██║ ██║
76
- █████╗ ██║██╔██╗ ██║██║ ███████║
77
- ██╔══╝ ██║██║╚██╗██║██║ ██╔══██║
78
- ██║ ██║██║ ╚████║╚██████╗██║ ██║
79
- ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝╚═╝ ╚═╝
80
- ${C.reset}`;
81
- function line(label, value, color = C.cyan) {
82
- const pad = " ".repeat(Math.max(0, 12 - label.length));
83
- process.stderr.write(` ${color}▸ ${label}${C.reset}${pad}${value}\n`);
84
- }
85
- function divider() {
86
- process.stderr.write(` ${C.dim}${"─".repeat(58)}${C.reset}\n`);
87
- }
88
- async function checkForUpdate(current) {
89
- try {
90
- const res = await fetch("https://registry.npmjs.org/@finchagentic/mcp/latest", {
91
- signal: AbortSignal.timeout(5000),
92
- });
93
- if (!res.ok)
94
- return;
95
- const data = await res.json();
96
- const latest = data.version;
97
- if (!latest || latest === current)
98
- return;
99
- // Show update notice to stderr - visible in Claude Desktop logs and terminal.
100
- const sep = ` ${"─".repeat(58)}`;
101
- process.stderr.write(`\n${sep}\n` +
102
- ` ${C.yellow}⚠${C.reset} Update available: ${C.yellow}v${current}${C.reset} → ${C.cyan}v${latest}${C.reset}\n` +
103
- ` ${C.dim}npm install -g @finchagentic/mcp@${latest}${C.reset} ${C.dim}or restart your MCP client${C.reset}\n` +
104
- `${sep}\n\n`);
105
- }
106
- catch {
107
- // Non-blocking - silently ignore network errors
108
- }
109
- }
110
- async function main() {
111
- (0, config_js_1.hydrateEnvFromConfig)();
112
- process.stderr.write(BANNER);
113
- const CAT_RULES = [
114
- { label: "Market", match: n => /^(get_market_data|get_token_data|compare_tokens|market_overview|token_history|get_base_token_data|stock_fundamentals|stock_insider|stock_events)$/.test(n) },
115
- { label: "Insight", match: n => /^(ask_finch|market_thesis|trade_plan)$/.test(n) },
116
- { label: "DeFi", match: n => n === "get_defi_yields" },
117
- { label: "Base MCP", match: n => n.startsWith("base_mcp_") },
118
- { label: "RH MCP", match: n => n.startsWith("rh_") },
119
- { label: "Automation", match: n => /^(create_automation|list_automations|pause_automation|delete_automation|get_automation_runs|run_automation)$/.test(n) },
120
- { label: "Vault", match: n => n.startsWith("vault_") || n === "code_session_save" || n === "list_projects" },
121
- { label: "Wallet", match: n => /^(get_wallet_address|get_wallet_balance|wallet_sign_message)$/.test(n) },
122
- { label: "Staking", match: n => /^(stake_finch|unstake_finch|stake_finch_status|stake_auto_restake|claim_vested_rewards)$/.test(n) },
123
- { label: "MiroShark", match: n => n.startsWith("miroshark_") },
124
- { label: "Scanner", match: n => /^(scan_market|score_token|check_token)$/.test(n) },
125
- { label: "Agents", match: n => n.startsWith("agent_") },
126
- { label: "Coder", match: n => n === "audit_contract" },
127
- { label: "Memory", match: n => n.startsWith("memory_") },
128
- { label: "OS", match: n => /^(finch_status|finch_diagnostics|finch_shell_chat)$/.test(n) },
129
- { label: "Research", match: n => /^(web_scrape|web_search|deep_research|research_compare|research_chain)$/.test(n) },
130
- { label: "Monitor", match: n => /^(schedule_research|list_monitors|cancel_monitor)$/.test(n) },
131
- { label: "GitHub", match: n => n.startsWith("github_") },
132
- { label: "Chronicle", match: n => n.startsWith("chronicle_") },
133
- { label: "Packets", match: n => n.startsWith("packet_") },
134
- ];
135
- const categories = CAT_RULES
136
- .map(rule => {
137
- const names = server_js_1.ALL_TOOLS.map(t => t.name).filter(rule.match);
138
- return { label: rule.label, count: names.length, tools: names.join(" · ") };
139
- })
140
- .filter(c => c.count > 0);
141
- const total = server_js_1.ALL_TOOLS.length;
142
- // What the connected client actually sees - the LIST response is filtered by
143
- // FINCH_TOOLS (default "core"). Showing this next to the registered total
144
- // stops the banner from claiming 121 while the client only sees the core set.
145
- const exposed = (0, tool_filter_js_1.filterTools)(server_js_1.ALL_TOOLS).length;
146
- const toolMode = process.env.FINCH_TOOLS ?? "core";
147
- divider();
148
- process.stderr.write(`\n`);
149
- // Tools don't run inference - the connected client's model does. A provider
150
- // is shown only because the CLI agent loop and scheduled agents use one; its
151
- // absence is the normal, fully-working state for an MCP client.
152
- const model = process.env.FINCH_MODEL ?? "claude-haiku-4-5-20251001";
153
- const aiMode = process.env.BANKR_API_KEY
154
- ? `Bankr ${C.dim}${model}${C.reset}`
155
- : process.env.ANTHROPIC_API_KEY
156
- ? `Anthropic ${C.dim}${model}${C.reset}`
157
- : process.env.OPENAI_API_KEY
158
- ? `OpenAI ${C.dim}${model}${C.reset}`
159
- : `your client's model ${C.dim}no key needed${C.reset}`;
160
- line("version", `v${PKG_VERSION}`);
161
- line("ai", aiMode);
162
- const toolDetail = exposed === total
163
- ? `${C.white}${C.bold}${total} tools exposed${C.reset} ${C.dim}across ${categories.length} categories${C.reset}`
164
- : `${C.white}${C.bold}${exposed} tools exposed${C.reset} ${C.dim}(${toolMode} of ${total} registered · set FINCH_TOOLS=all for every tool)${C.reset}`;
165
- line("tools", toolDetail);
166
- process.stderr.write(`\n`);
167
- divider();
168
- process.stderr.write(`\n`);
169
- // ── Categories grid ────────────────────────────────────────────────────────
170
- for (const cat of categories) {
171
- const countStr = `${cat.count}`.padStart(2);
172
- process.stderr.write(` ${C.dim}│${C.reset} ${C.cyan}${cat.label.padEnd(11)}${C.reset} ${C.dim}${countStr}x${C.reset} ${C.dim}${cat.tools}${C.reset}\n`);
173
- }
174
- process.stderr.write(`\n`);
175
- divider();
176
- // ── Wallet + start ─────────────────────────────────────────────────────────
177
- await (0, server_js_1.startServer)();
178
- const hasAuth = !!(0, config_js_1.getSavedToken)();
179
- try {
180
- const wallet = await (0, wallet_js_1.getOrCreateWallet)();
181
- process.stderr.write(`\n`);
182
- line("wallet", wallet.address);
183
- if (hasAuth) {
184
- line("auth", `${C.green}signed in${C.reset} ${C.dim}all ${server_js_1.ALL_TOOLS.length} tools unlocked${C.reset}`, C.green);
185
- }
186
- else {
187
- line("auth", `${C.yellow}not signed in${C.reset} ${C.dim}run 'finch login' to unlock premium tools${C.reset}`, C.yellow);
188
- }
189
- line("status", `${C.green}ready${C.reset} ${C.dim}waiting for MCP client...${C.reset}`, C.green);
190
- process.stderr.write(`\n`);
191
- }
192
- catch {
193
- process.stderr.write(`\n`);
194
- line("wallet", `${C.yellow}not configured${C.reset} ${C.dim}run 'finch login' to set up${C.reset}`, C.yellow);
195
- line("status", `${C.green}ready${C.reset} ${C.dim}wallet tools require setup${C.reset}`, C.green);
196
- process.stderr.write(`\n`);
197
- }
198
- // Check for updates async - fires 3s after startup so it doesn't delay boot
199
- setTimeout(() => { checkForUpdate(PKG_VERSION).catch(() => { }); }, 3000);
200
- }
201
- // Interactive sign-in for the `finch-mcp login` path. Kept identical to the
202
- // canonical `finch login` (cli.ts): both accept a Finch API key
203
- // (finch_sk_...) and exchange it for a session token at /auth/apikey/login.
204
- // Earlier this asked for a raw "session token" against /auth/me - a different
205
- // credential users never actually generate, so the two bins disagreed on what
206
- // "login" meant.
207
- async function loginFlow() {
208
- process.stderr.write(`\n ${C.cyan}▸ finch login${C.reset}\n\n`);
209
- process.stderr.write(` Generate an API key at ${C.cyan}app.finchagentic.com${C.reset} → Settings → API Keys\n\n`);
210
- const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
211
- let apiKey = await new Promise((resolve) => {
212
- rl.question(` API key (finch_sk_... / noel_sk_...): `, (answer) => {
213
- rl.close();
214
- resolve(answer.trim());
215
- });
216
- });
217
- // Deduplicate doubled input from a known Clink terminal bug + strip non-ASCII.
218
- apiKey = (0, clink_input_js_1.dedupClinkInput)(apiKey).replace(/[^\x20-\x7E]/g, "").trim();
219
- if (!(0, config_js_1.isApiKey)(apiKey)) {
220
- process.stderr.write(`\n ${C.yellow}✗ Invalid key — should start with finch_sk_ or noel_sk_${C.reset}\n\n`);
221
- process.exit(1);
222
- }
223
- try {
224
- const res = await fetch(`${CONVEX_SITE}/auth/apikey/login`, {
225
- method: "POST",
226
- headers: { "Content-Type": "application/json" },
227
- body: JSON.stringify({ apiKey }),
228
- signal: AbortSignal.timeout(8000),
229
- });
230
- const data = await res.json();
231
- if (!res.ok || !data.token) {
232
- process.stderr.write(`\n ${C.yellow}✗ ${data.error ?? "Invalid API key"} — check it at app.finchagentic.com${C.reset}\n\n`);
233
- process.exit(1);
234
- }
235
- (0, config_js_1.writeConfig)({ sessionToken: data.token, email: data.email, name: data.displayName ?? undefined });
236
- process.stderr.write(`\n ${C.green}✓ Signed in as ${data.email ?? data.displayName ?? "user"}${C.reset}\n`);
237
- process.stderr.write(` ${C.dim}Token saved to ~/.finch/config.json${C.reset}\n`);
238
- process.stderr.write(` ${C.dim}All ${server_js_1.ALL_TOOLS.length} tools now unlocked.${C.reset}\n\n`);
239
- }
240
- catch (err) {
241
- process.stderr.write(`\n ${C.red}✗ Login failed: ${err.message}${C.reset}\n\n`);
242
- process.exit(1);
243
- }
244
- process.exit(0);
245
- }
246
- const cmd = process.argv[2];
247
- if (cmd === "login") {
248
- loginFlow().catch((err) => {
249
- process.stderr.write(`[finch] login error: ${err}\n`);
250
- process.exit(1);
251
- });
252
- }
253
- else {
254
- main().catch((err) => {
255
- process.stderr.write(`[finch] fatal: ${err}\n`);
256
- process.exit(1);
257
- });
258
- }