@finchagentic/mcp 4.6.2 → 4.6.3

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 (59) hide show
  1. package/README.md +39 -74
  2. package/dist/_http-cache.js +96 -0
  3. package/dist/_text-search.js +39 -0
  4. package/dist/agent-loop.js +301 -0
  5. package/dist/annotations.js +122 -0
  6. package/dist/cli.js +1391 -0
  7. package/dist/clink-input.js +15 -0
  8. package/dist/config.js +132 -0
  9. package/dist/convex.js +175 -0
  10. package/dist/dex-pair.js +54 -0
  11. package/dist/enrichment-router.js +315 -0
  12. package/dist/index.js +258 -0
  13. package/dist/llm.js +298 -0
  14. package/dist/local-memory-file.js +150 -0
  15. package/dist/local-memory.js +135 -0
  16. package/dist/local-vault.js +456 -0
  17. package/dist/output-schemas.js +605 -0
  18. package/dist/project.js +36 -0
  19. package/dist/prompts.js +111 -0
  20. package/dist/public-url.js +107 -0
  21. package/dist/resources.js +111 -0
  22. package/dist/server.js +322 -0
  23. package/dist/signal-gate.js +57 -0
  24. package/dist/token-decimals.js +26 -0
  25. package/dist/token-gate.js +88 -0
  26. package/dist/tool-filter.js +53 -0
  27. package/dist/tools/_solidity-scan.js +313 -0
  28. package/dist/tools/agents.js +441 -0
  29. package/dist/tools/automation.js +354 -0
  30. package/dist/tools/base-mcp.js +466 -0
  31. package/dist/tools/base.js +283 -0
  32. package/dist/tools/chronicle.js +268 -0
  33. package/dist/tools/coder.js +94 -0
  34. package/dist/tools/deep-research.js +1421 -0
  35. package/dist/tools/defi.js +292 -0
  36. package/dist/tools/equity.js +372 -0
  37. package/dist/tools/events.js +182 -0
  38. package/dist/tools/github.js +564 -0
  39. package/dist/tools/insider.js +264 -0
  40. package/dist/tools/insight.js +630 -0
  41. package/dist/tools/market.js +555 -0
  42. package/dist/tools/memory.js +1059 -0
  43. package/dist/tools/miroshark.js +350 -0
  44. package/dist/tools/monitor.js +319 -0
  45. package/dist/tools/os.js +236 -0
  46. package/dist/tools/packets.js +296 -0
  47. package/dist/tools/research-chain.js +226 -0
  48. package/dist/tools/research-compare.js +280 -0
  49. package/dist/tools/research.js +188 -0
  50. package/dist/tools/rh-bridge.js +148 -0
  51. package/dist/tools/rh-mcp.js +1448 -0
  52. package/dist/tools/rh-orders.js +556 -0
  53. package/dist/tools/scanner.js +564 -0
  54. package/dist/tools/stake.js +369 -0
  55. package/dist/tools/vault.js +1020 -0
  56. package/dist/tools/wallet.js +200 -0
  57. package/dist/types.js +2 -0
  58. package/dist/wallet.js +372 -0
  59. package/package.json +4 -7
@@ -0,0 +1,188 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RESEARCH_TOOLS = void 0;
4
+ exports.handleResearchTool = handleResearchTool;
5
+ const zod_1 = require("zod");
6
+ const convex_js_1 = require("../convex.js");
7
+ const public_url_js_1 = require("../public-url.js");
8
+ const FC_BASE = "https://api.firecrawl.dev/v1";
9
+ exports.RESEARCH_TOOLS = [
10
+ {
11
+ name: "web_scrape",
12
+ description: "Fetch and extract clean readable content from any URL - returns markdown. " +
13
+ "Use when an agent needs to read an article, docs page, GitHub repo, or any web page. " +
14
+ "Set FIRECRAWL_API_KEY for best quality (firecrawl.dev). Falls back to basic fetch if not set.",
15
+ inputSchema: {
16
+ type: "object",
17
+ properties: {
18
+ url: { type: "string", description: "URL to fetch" },
19
+ focus: { type: "string", description: "Optional: specific topic or section to extract from the page" },
20
+ },
21
+ required: ["url"],
22
+ },
23
+ },
24
+ {
25
+ name: "web_search",
26
+ description: "Search the web and return raw results: titles, URLs, and snippets. No synthesis or analysis — " +
27
+ "use this for quick lookups, finding sources, or fetching recent news. " +
28
+ "For multi-source research with LLM synthesis, use deep_research instead. " +
29
+ "Requires FIRECRAWL_API_KEY (firecrawl.dev).",
30
+ inputSchema: {
31
+ type: "object",
32
+ properties: {
33
+ query: { type: "string", description: "Search query" },
34
+ limit: { type: "number", description: "Max results (default 5, max 10)" },
35
+ },
36
+ required: ["query"],
37
+ },
38
+ },
39
+ ];
40
+ const ScrapeSchema = zod_1.z.object({
41
+ url: zod_1.z.string().url(),
42
+ focus: zod_1.z.string().optional(),
43
+ });
44
+ const SearchSchema = zod_1.z.object({
45
+ query: zod_1.z.string().min(1),
46
+ limit: zod_1.z.number().int().min(1).max(10).optional(),
47
+ });
48
+ async function firecrawlScrape(url) {
49
+ // Priority 1: user's own FIRECRAWL_API_KEY (BYOK fast-path, zero network detour).
50
+ const key = process.env.FIRECRAWL_API_KEY;
51
+ if (key) {
52
+ try {
53
+ const res = await fetch(`${FC_BASE}/scrape`, {
54
+ method: "POST",
55
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` },
56
+ body: JSON.stringify({ url, formats: ["markdown"], onlyMainContent: true }),
57
+ signal: AbortSignal.timeout(20000),
58
+ });
59
+ if (res.ok) {
60
+ const data = await res.json();
61
+ return data.data?.markdown ?? null;
62
+ }
63
+ }
64
+ catch { /* fall through to backend proxy */ }
65
+ }
66
+ // Priority 2: route through Finch backend (session-token authed, backend
67
+ // pays for the Firecrawl call). The backend returns 503 if it doesn't have
68
+ // FIRECRAWL_API_KEY set either, in which case web_scrape silently falls
69
+ // through to basicFetch below.
70
+ try {
71
+ const data = await (0, convex_js_1.callConvex)("/research/firecrawl-scrape", "POST", { url }, "web_scrape", 25000);
72
+ if (data?.markdown)
73
+ return data.markdown;
74
+ }
75
+ catch { /* fall through */ }
76
+ return null;
77
+ }
78
+ async function basicFetch(url) {
79
+ try {
80
+ const res = await fetch(url, {
81
+ headers: { "User-Agent": "Mozilla/5.0 (compatible; FinchBot/1.0)" },
82
+ signal: AbortSignal.timeout(10000),
83
+ });
84
+ if (!res.ok)
85
+ return null;
86
+ const html = await res.text();
87
+ return html
88
+ .replace(/<script[\s\S]*?<\/script>/gi, "")
89
+ .replace(/<style[\s\S]*?<\/style>/gi, "")
90
+ .replace(/<[^>]+>/g, " ")
91
+ .replace(/\s+/g, " ")
92
+ .trim()
93
+ .slice(0, 4000);
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ }
99
+ async function handleResearchTool(name, args) {
100
+ if (name === "web_scrape") {
101
+ const parsed = ScrapeSchema.safeParse(args);
102
+ if (!parsed.success)
103
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
104
+ const { url, focus } = parsed.data;
105
+ const unsafe = await (0, public_url_js_1.assertPublicUrl)(url);
106
+ if (unsafe) {
107
+ return { content: [{ type: "text", text: (0, public_url_js_1.refuseUrlText)(url, unsafe) }], isError: true };
108
+ }
109
+ let content = await firecrawlScrape(url);
110
+ const source = content ? "Firecrawl" : "basic fetch";
111
+ if (!content)
112
+ content = await basicFetch(url);
113
+ if (!content) {
114
+ return { content: [{ type: "text", text: `Could not fetch ${url} - page may require JavaScript or block crawlers.` }], isError: true };
115
+ }
116
+ const focusNote = focus ? `\n\n_Focus: ${focus}_\n\n` : "\n\n";
117
+ const body = content.length > 6000 ? content.slice(0, 6000) + "\n\n…(truncated)" : content;
118
+ return { content: [{ type: "text", text: `**${url}**${focusNote}${body}\n\n_Source: ${source}_` }] };
119
+ }
120
+ if (name === "web_search") {
121
+ const parsed = SearchSchema.safeParse(args);
122
+ if (!parsed.success)
123
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
124
+ const { query, limit = 5 } = parsed.data;
125
+ // Priority 1: user BYOK direct path. Priority 2: Finch proxy.
126
+ // Same data shape so the formatting code below is identical for both.
127
+ const key = process.env.FIRECRAWL_API_KEY;
128
+ let results = null;
129
+ let lastErr = null;
130
+ if (key) {
131
+ try {
132
+ const res = await fetch(`${FC_BASE}/search`, {
133
+ method: "POST",
134
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` },
135
+ body: JSON.stringify({ query, limit }),
136
+ signal: AbortSignal.timeout(15000),
137
+ });
138
+ if (res.ok) {
139
+ const data = await res.json();
140
+ results = data.data ?? [];
141
+ }
142
+ else {
143
+ lastErr = `${res.status}: ${(await res.text()).slice(0, 200)}`;
144
+ }
145
+ }
146
+ catch (err) {
147
+ lastErr = err?.message ?? "BYOK search failed";
148
+ }
149
+ }
150
+ if (!results) {
151
+ try {
152
+ const data = await (0, convex_js_1.callConvex)("/research/firecrawl-search", "POST", { query, limit }, "web_search", 30000);
153
+ results = data?.results ?? [];
154
+ }
155
+ catch (err) {
156
+ const msg = err?.message ?? String(err);
157
+ return {
158
+ content: [{
159
+ type: "text",
160
+ text: [
161
+ `web_search failed.`,
162
+ ``,
163
+ `Tried backend proxy: ${msg.slice(0, 200)}`,
164
+ lastErr ? `Direct call also failed: ${lastErr}` : "",
165
+ ``,
166
+ `Fix: either run \`/login\` so the Finch backend can pay, or set FIRECRAWL_API_KEY in your MCP env block.`,
167
+ ].filter(Boolean).join("\n"),
168
+ }],
169
+ isError: true,
170
+ };
171
+ }
172
+ }
173
+ const safeResults = results ?? [];
174
+ if (!safeResults.length) {
175
+ return { content: [{ type: "text", text: `No results found for: "${query}"` }] };
176
+ }
177
+ const lines = [`🔍 **Web Search: "${query}"** - ${safeResults.length} results\n`];
178
+ for (const r of safeResults) {
179
+ lines.push(`**${r.title ?? r.url}**`);
180
+ lines.push(r.url);
181
+ if (r.description)
182
+ lines.push(r.description.slice(0, 200));
183
+ lines.push("");
184
+ }
185
+ return { content: [{ type: "text", text: lines.join("\n") }] };
186
+ }
187
+ return null;
188
+ }
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ // Tokenized stock ↔ real equity comparison on Robinhood Chain.
3
+ //
4
+ // Robinhood Chain exists to trade equities 24/7 on-chain, but the two prices
5
+ // drift: the token trades continuously against a shallow pool while the real
6
+ // share only prices during market hours. That gap is the whole point of the
7
+ // bridge — and it matters to both sides. An equity trader wants to know whether
8
+ // after-hours exposure is fairly priced; an on-chain trader wants to know when
9
+ // the token has detached from the asset it represents.
10
+ //
11
+ // The gap alone is not an opportunity: a 20% premium on a $100k pool you cannot
12
+ // exit is a trap, so pool depth is reported next to every quote.
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.BRIDGE_TOOLS = void 0;
15
+ exports.handleBridgeTool = handleBridgeTool;
16
+ const zod_1 = require("zod");
17
+ const rh_mcp_js_1 = require("./rh-mcp.js");
18
+ exports.BRIDGE_TOOLS = [
19
+ {
20
+ name: "rh_stock_bridge",
21
+ description: "Compare a tokenized stock on Robinhood Chain (4663) against the real US equity: " +
22
+ "on-chain price vs live share price, the premium/discount between them, pool depth, " +
23
+ "and whether the US market is currently open. Covers the 22 tokenized tickers " +
24
+ "(NVDA, TSLA, AAPL, COIN…). Omit `ticker` to scan all of them at once. " +
25
+ "No API key needed. Reports the gap and the depth — it does not tell you to trade it.",
26
+ inputSchema: {
27
+ type: "object",
28
+ properties: {
29
+ ticker: {
30
+ type: "string",
31
+ description: "Tokenized stock symbol, e.g. 'NVDA'. Omit to scan every tokenized stock.",
32
+ },
33
+ minGapPct: {
34
+ type: "number",
35
+ description: "When scanning, only show names whose gap exceeds this (default 0 = show all)",
36
+ },
37
+ },
38
+ required: [],
39
+ },
40
+ },
41
+ ];
42
+ const Schema = zod_1.z.object({
43
+ ticker: zod_1.z.string().min(1).max(10).optional(),
44
+ minGapPct: zod_1.z.number().min(0).max(100).optional(),
45
+ });
46
+ async function realQuote(symbol) {
47
+ try {
48
+ const res = await fetch(`https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(symbol)}?interval=1d&range=1d`, { headers: { "User-Agent": "Mozilla/5.0" }, signal: AbortSignal.timeout(12000) });
49
+ if (!res.ok)
50
+ return null;
51
+ const j = await res.json();
52
+ const m = j?.chart?.result?.[0]?.meta;
53
+ if (!m?.regularMarketPrice)
54
+ return null;
55
+ // The chart endpoint carries no `marketState`, so derive it: during the
56
+ // regular session `regularMarketTime` keeps advancing, and once the bell
57
+ // rings it freezes at the close. A recent timestamp means live pricing.
58
+ const ts = Number(m.regularMarketTime) * 1000;
59
+ const ageMin = isFinite(ts) ? (Date.now() - ts) / 60000 : Infinity;
60
+ return { price: m.regularMarketPrice, state: ageMin <= 20 ? "REGULAR" : "CLOSED" };
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ async function buildRow(s) {
67
+ const [dex, rq] = await Promise.all([(0, rh_mcp_js_1.dexTokenBest)(s.address), realQuote(s.symbol)]);
68
+ const onchain = dex?.priceUsd != null ? Number(dex.priceUsd) : null;
69
+ const real = rq?.price ?? null;
70
+ const gapPct = onchain != null && real != null && real > 0 ? ((onchain - real) / real) * 100 : null;
71
+ return {
72
+ symbol: s.symbol,
73
+ name: s.name,
74
+ address: s.address,
75
+ onchain: onchain != null && isFinite(onchain) ? onchain : null,
76
+ real,
77
+ gapPct,
78
+ liquidityUsd: dex?.liquidity?.usd ?? 0,
79
+ marketState: rq?.state,
80
+ };
81
+ }
82
+ function gapTag(gap, liq) {
83
+ const a = Math.abs(gap);
84
+ if (a < 1)
85
+ return "🟢 tracking";
86
+ if (a < 3)
87
+ return "🟡 minor drift";
88
+ if (liq < 50000)
89
+ return "🟠 wide — but the pool is thin";
90
+ return "🔴 wide";
91
+ }
92
+ async function handleBridgeTool(name, args) {
93
+ if (name !== "rh_stock_bridge")
94
+ return null;
95
+ const parsed = Schema.safeParse(args);
96
+ if (!parsed.success) {
97
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
98
+ }
99
+ const wanted = parsed.data.ticker?.trim().toUpperCase();
100
+ const minGap = parsed.data.minGapPct ?? 0;
101
+ const targets = wanted ? rh_mcp_js_1.RH_STOCKS.filter((s) => s.symbol === wanted) : rh_mcp_js_1.RH_STOCKS;
102
+ if (wanted && targets.length === 0) {
103
+ return {
104
+ content: [{
105
+ type: "text",
106
+ text: `**${wanted}** is not a tokenized stock on Robinhood Chain.\n\n` +
107
+ `Available: ${rh_mcp_js_1.RH_STOCKS.map((s) => s.symbol).join(", ")}`,
108
+ }],
109
+ isError: true,
110
+ };
111
+ }
112
+ const rows = await Promise.all(targets.map(buildRow));
113
+ const usable = rows.filter((r) => r.gapPct != null);
114
+ const marketState = usable.find((r) => r.marketState)?.marketState ?? "UNKNOWN";
115
+ const marketOpen = marketState === "REGULAR";
116
+ const lines = [
117
+ `# Tokenized stock ↔ real equity — Robinhood Chain (4663)`,
118
+ ``,
119
+ `**US market: ${marketOpen ? "🟢 OPEN" : "🔴 CLOSED"}**` +
120
+ (marketOpen
121
+ ? ` — both sides are pricing live, so a gap is a genuine dislocation.`
122
+ : ` — the share price is frozen at the last close while the token keeps trading. ` +
123
+ `A gap here is largely expected and is NOT by itself a mispricing.`),
124
+ ``,
125
+ ];
126
+ // Single-ticker detail view.
127
+ if (wanted && usable.length === 1) {
128
+ const r = usable[0];
129
+ lines.push(`## ${r.symbol} — ${r.name}`, ``, `| | |`, `|---|---|`, `| On-chain (token) | **$${r.onchain.toFixed(4)}** |`, `| Real share | **$${r.real.toFixed(2)}** |`, `| Gap | **${r.gapPct >= 0 ? "+" : ""}${r.gapPct.toFixed(2)}%** ${gapTag(r.gapPct, r.liquidityUsd)} |`, `| Pool depth | $${Math.round(r.liquidityUsd).toLocaleString()} |`, `| Token | \`${r.address}\` |`, ``, `${rh_mcp_js_1.RH_EXPLORER}/token/${r.address}`, ``);
130
+ }
131
+ else {
132
+ const shown = usable
133
+ .filter((r) => Math.abs(r.gapPct) >= minGap)
134
+ .sort((a, b) => Math.abs(b.gapPct) - Math.abs(a.gapPct));
135
+ lines.push(`| Ticker | On-chain | Real | Gap | Pool depth |`, `|---|---|---|---|---|`, ...shown.map((r) => `| **${r.symbol}** | $${r.onchain.toFixed(2)} | $${r.real.toFixed(2)} | ` +
136
+ `${r.gapPct >= 0 ? "+" : ""}${r.gapPct.toFixed(2)}% ${gapTag(r.gapPct, r.liquidityUsd)} | ` +
137
+ `$${Math.round(r.liquidityUsd).toLocaleString()} |`), ``);
138
+ const missing = rows.filter((r) => r.gapPct == null);
139
+ if (missing.length) {
140
+ lines.push(`_No usable quote for: ${missing.map((m) => m.symbol).join(", ")}._`, ``);
141
+ }
142
+ }
143
+ lines.push(`---`, ``, `**Reading this**`, ``, `- A gap is only tradeable to the depth of the pool. A 20% premium on a $100k pool is not ` +
144
+ `a 20% opportunity — size it against the depth column, then check the real fill with \`rh_mcp_estimate\`.`, `- While the US market is closed the token is the only live price. Drift is expected, and it ` +
145
+ `often reflects overnight news the share price has not opened to yet.`, `- Buying the token is exposure to the token, not ownership of the share. Redemption terms, ` +
146
+ `custody and counterparty are Robinhood's, not this tool's — and are not verified here.`, ``, `_Prices: on-chain via DexScreener, share via Yahoo Finance. Not investment advice._`);
147
+ return { content: [{ type: "text", text: lines.join("\n") }] };
148
+ }