@finchagentic/mcp 4.0.0 → 4.2.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.
@@ -4,10 +4,35 @@ exports.BASE_TOOLS = void 0;
4
4
  exports.handleBaseTool = handleBaseTool;
5
5
  const MORPHO_API = "https://blue-api.morpho.org/graphql";
6
6
  const MOONWELL_API = "https://api.moonwell.fi/v1/markets";
7
+ /** Morpho's public API occasionally 403s transiently (rate-limit/WAF blip,
8
+ * confirmed by hand: identical requests succeed moments later) - one retry
9
+ * after a short delay clears most of these without adding real latency to
10
+ * the common case. */
11
+ async function fetchMorphoGql(query) {
12
+ for (let attempt = 0; attempt < 2; attempt++) {
13
+ if (attempt > 0)
14
+ await new Promise((r) => setTimeout(r, 800));
15
+ const res = await fetch(MORPHO_API, {
16
+ method: "POST",
17
+ headers: { "Content-Type": "application/json" },
18
+ body: JSON.stringify({ query }),
19
+ signal: AbortSignal.timeout(15000),
20
+ });
21
+ if (res.ok)
22
+ return res.json();
23
+ if (attempt === 1)
24
+ throw new Error(`Morpho API error: ${res.status} (after retry)`);
25
+ }
26
+ }
7
27
  exports.BASE_TOOLS = [
8
28
  {
9
29
  name: "base_mcp_yield_vaults",
10
- description: "Find the best yield/earning opportunities on Base chain using Morpho vaults. Returns all vaults ranked by APY — use this when the user asks about yield farming, APY, best rates to earn, where to put USDC, or passive income on Base. For yield on a specific token, also see base_mcp_lend.",
30
+ description: "Find the best yield/earning opportunities on Base chain using Morpho vaults - queries Morpho's own " +
31
+ "API directly, so numbers are fresher than get_defi_yields' DeFiLlama-aggregated Morpho figures, " +
32
+ "but this tool only covers Morpho (not Aerodrome/Uniswap/other protocols - use get_defi_yields for " +
33
+ "cross-protocol comparison). Returns all vaults ranked by APY — use this when the user asks about " +
34
+ "yield farming, APY, best rates to earn, where to put USDC, or passive income on Base. For yield on " +
35
+ "a specific token, also see base_mcp_lend.",
11
36
  inputSchema: {
12
37
  type: "object",
13
38
  properties: {
@@ -25,7 +50,12 @@ exports.BASE_TOOLS = [
25
50
  },
26
51
  {
27
52
  name: "base_mcp_lending_rates",
28
- description: "Get lending and borrowing rates across all Moonwell markets on Base. Returns supply APY, borrow APY, liquidity, and utilization per asset. Use when the user asks about borrow rates, lending rates, interest rates, or supply APY on Base.",
53
+ description: "Get lending and borrowing rates across all Moonwell markets on Base - queries Moonwell's own API " +
54
+ "directly, so numbers are fresher than get_defi_yields' DeFiLlama-aggregated Moonwell figures, but " +
55
+ "this tool only covers Moonwell (use get_defi_yields for cross-protocol comparison, or " +
56
+ "base_mcp_yield_vaults for Morpho specifically). Returns supply APY, borrow APY, liquidity, and " +
57
+ "utilization per asset. Use when the user asks about borrow rates, lending rates, interest rates, " +
58
+ "or supply APY on Base.",
29
59
  inputSchema: {
30
60
  type: "object",
31
61
  properties: {
@@ -99,15 +129,7 @@ async function fetchMorphoVaults(asset, limit = 10) {
99
129
  }
100
130
  }
101
131
  }`;
102
- const res = await fetch(MORPHO_API, {
103
- method: "POST",
104
- headers: { "Content-Type": "application/json" },
105
- body: JSON.stringify({ query: gql }),
106
- signal: AbortSignal.timeout(15000),
107
- });
108
- if (!res.ok)
109
- throw new Error(`Morpho API error: ${res.status}`);
110
- const data = await res.json();
132
+ const data = await fetchMorphoGql(gql);
111
133
  let vaults = data?.data?.vaults?.items ?? [];
112
134
  // Filter out test/spam vaults: min $10k TVL, max 500% APY
113
135
  vaults = vaults.filter((v) => {
@@ -199,15 +221,7 @@ async function prepareDeposit(vaultName, asset, amount) {
199
221
  }
200
222
  }
201
223
  }`;
202
- const res = await fetch(MORPHO_API, {
203
- method: "POST",
204
- headers: { "Content-Type": "application/json" },
205
- body: JSON.stringify({ query: gql }),
206
- signal: AbortSignal.timeout(15000),
207
- });
208
- if (!res.ok)
209
- throw new Error(`Morpho API error: ${res.status}`);
210
- const data = await res.json();
224
+ const data = await fetchMorphoGql(gql);
211
225
  let vaults = data?.data?.vaults?.items ?? [];
212
226
  // Filter by asset first
213
227
  vaults = vaults.filter((v) => v.asset?.symbol?.toLowerCase() === asset.toLowerCase());
@@ -10,7 +10,7 @@ const CHRONICLE_TYPES = ["vault", "memory", "agent", "tool", "automation", "moni
10
10
  exports.CHRONICLE_TOOLS = [
11
11
  {
12
12
  name: "chronicle_add",
13
- description: "Log an event to Noel Chronicle - the system-wide audit log for your AI runtime. " +
13
+ description: "Log an event to Finch Chronicle - the system-wide audit log for your AI runtime. " +
14
14
  "Records anything meaningful: vault saves, agent updates, automation triggers, " +
15
15
  "custom milestones, research completions. Chronicle is your permanent timeline of what happened. " +
16
16
  "Types: vault | memory | agent | tool | automation | monitor | system | custom.",
@@ -40,7 +40,7 @@ exports.CHRONICLE_TOOLS = [
40
40
  },
41
41
  {
42
42
  name: "chronicle_list",
43
- description: "Read the Noel Chronicle event log - your AI runtime timeline. Returns recent events in reverse chronological order. " +
43
+ description: "Read the Finch Chronicle event log - your AI runtime timeline. Returns recent events in reverse chronological order. " +
44
44
  "Filter by type to see only vault saves, agent activity, automations, etc.",
45
45
  inputSchema: {
46
46
  type: "object",
@@ -59,7 +59,7 @@ exports.CHRONICLE_TOOLS = [
59
59
  },
60
60
  {
61
61
  name: "chronicle_search",
62
- description: "Search the Noel Chronicle by keyword. Matches against event titles and details. " +
62
+ description: "Search the Finch Chronicle by keyword. Matches against event titles and details. " +
63
63
  "Useful for finding when something specific happened: 'when did I last research ETH?' or 'find all vault saves for Base'.",
64
64
  inputSchema: {
65
65
  type: "object",
@@ -187,7 +187,7 @@ async function handleChronicle(name, args) {
187
187
  };
188
188
  }
189
189
  const lines = [
190
- `## 📜 Noel Chronicle${type ? ` · ${type}` : ""}`,
190
+ `## 📜 Finch Chronicle${type ? ` · ${type}` : ""}`,
191
191
  `*${entries.length} event${entries.length !== 1 ? "s" : ""}*`,
192
192
  "",
193
193
  ];
@@ -267,7 +267,7 @@ async function fcSearch(query, limit) {
267
267
  }
268
268
  // Backend-proxy path - session-authed; Finch covers Firecrawl cost.
269
269
  try {
270
- const data = await (0, convex_js_1.callConvex)("/research/firecrawl-search", "POST", { query, limit }, "deep_research_search_proxy", 20000);
270
+ const data = await (0, convex_js_1.callConvex)("/research/firecrawl-search", "POST", { query, limit }, "web_search", 20000);
271
271
  return normalizeSearchHits(data?.results);
272
272
  }
273
273
  catch {
@@ -300,7 +300,7 @@ async function fcScrape(url) {
300
300
  // which means continueFrom can't auto-date proxied scrapes. Acceptable
301
301
  // tradeoff for now; markdown is the primary signal.
302
302
  try {
303
- const data = await (0, convex_js_1.callConvex)("/research/firecrawl-scrape", "POST", { url }, "deep_research_scrape_proxy", 25000);
303
+ const data = await (0, convex_js_1.callConvex)("/research/firecrawl-scrape", "POST", { url }, "web_scrape", 25000);
304
304
  if (data?.markdown)
305
305
  return { markdown: data.markdown };
306
306
  }
@@ -568,7 +568,7 @@ async function synthesize(query, sources, isFinal, liveSearch, priorContext, fre
568
568
  let memoryContext = "";
569
569
  if (isFinal) {
570
570
  const [profileData, memHits] = await Promise.allSettled([
571
- (0, convex_js_1.callConvex)("/vault/profile-context?maxChars=1200", "GET", undefined, "profile_context"),
571
+ (0, convex_js_1.callConvex)("/vault/profile-context?maxChars=1200", "GET", undefined, "vault_read"),
572
572
  (0, memory_js_1.searchSupermemory)(query, 4),
573
573
  ]);
574
574
  if (profileData.status === "fulfilled") {
@@ -744,7 +744,12 @@ Write the ${isFinal ? "final" : "draft"} report now. Markdown only - no preamble
744
744
  // pick for reasoning, JSON, code). Override via env or pass the same
745
745
  // model string to FINCH_MODEL to bypass.
746
746
  const researchModel = process.env.FINCH_RESEARCH_MODEL ?? "grok-4.3";
747
- const raw = await (0, llm_js_1.callLLM)(sys, user, isFinal ? 4000 : 2000, [], 90000, { liveSearch, model: researchModel });
747
+ // Deep mode (critic notes present, or the 5-angle planner ran) produces a
748
+ // longer report than the standard 6-section template - a fixed 4000-token
749
+ // budget was cutting deep reports off mid-sentence around risk #6-7 of 13+.
750
+ const isDeepMode = !!criticNotes || (angles?.length ?? 0) >= 5;
751
+ const finalTokens = isDeepMode ? 7000 : 4000;
752
+ const raw = await (0, llm_js_1.callLLM)(sys, user, isFinal ? finalTokens : 2000, [], 90000, { liveSearch, model: researchModel });
748
753
  const { content: report, liveCitations } = extractLiveCitations(raw);
749
754
  // Citation density check - only for final reports. If the report has many
750
755
  // numerical claims but very few [N] citations, retry once with a stricter
@@ -757,7 +762,7 @@ Write the ${isFinal ? "final" : "draft"} report now. Markdown only - no preamble
757
762
 
758
763
  ⚠️ Your previous draft had ${density.numericalClaims} numerical claims but only ${density.citations} [N] citations. That ratio is too low. Rewrite with stricter citation density: every percentage, dollar amount, count, date, and named entity must carry [N]. Use the At a Glance table to anchor the key metrics.`;
759
764
  try {
760
- const rawRetry = await (0, llm_js_1.callLLM)(sys, retryUser, 4000, [], 90000, { liveSearch, model: researchModel });
765
+ const rawRetry = await (0, llm_js_1.callLLM)(sys, retryUser, finalTokens, [], 90000, { liveSearch, model: researchModel });
761
766
  const { content: retryReport, liveCitations: retryCitations } = extractLiveCitations(rawRetry);
762
767
  return { report: retryReport, liveCitations: retryCitations.length > 0 ? retryCitations : liveCitations };
763
768
  }
@@ -11,7 +11,11 @@ exports.DEFI_TOOLS = [
11
11
  {
12
12
  name: "get_defi_yields",
13
13
  description: "Fetch top DeFi yield opportunities on Base - Morpho, Moonwell, Aerodrome, Uniswap, and more. " +
14
- "Returns live APY, TVL, and pool info from DeFiLlama (no API key required). " +
14
+ "Returns APY, TVL, and pool info aggregated from DeFiLlama (no API key required) - broadest " +
15
+ "protocol coverage, but DeFiLlama's numbers can lag the protocol's own API by hours. Good first " +
16
+ "stop for comparing across protocols. For the freshest Morpho-vault-specific numbers use " +
17
+ "base_mcp_yield_vaults instead; for the freshest Moonwell supply/borrow rates use " +
18
+ "base_mcp_lending_rates instead - both query the protocol directly. " +
15
19
  "Filter by token or minimum APY. Use before depositing to find the best rates.",
16
20
  inputSchema: {
17
21
  type: "object",
@@ -34,7 +38,6 @@ const SwapSchema = zod_1.z.object({
34
38
  const DEFAULT_MAX_SLIPPAGE_PCT = 1.0;
35
39
  const DEFAULT_MAX_PRICE_IMPACT_PCT = 3.0;
36
40
  const SendSchema = zod_1.z.object({ token: zod_1.z.string().min(1), toAddress: zod_1.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "must be a valid 0x address"), amount: zod_1.z.string().min(1) });
37
- const AnalyzeWalletSchema = zod_1.z.object({ address: zod_1.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "must be a valid 0x address"), label: zod_1.z.string().optional() });
38
41
  const DefiYieldsSchema = zod_1.z.object({
39
42
  token: zod_1.z.string().optional(),
40
43
  minApy: zod_1.z.number().optional(),
@@ -103,7 +106,7 @@ async function handleDefiTool(name, args) {
103
106
  // Pass the MCP local wallet as the swap taker so 0x routes output back
104
107
  // to the wallet that's signing - not the backend's custodial wallet.
105
108
  const localWallet = await (0, wallet_js_1.getOrCreateWallet)();
106
- const result = await (0, convex_js_1.callConvex)("/mcp/defi/swap", "POST", { fromToken, toToken, amount, taker: localWallet.address, slippagePercentage: slippageLimit / 100 }, "estimate_swap");
109
+ const result = await (0, convex_js_1.callConvex)("/mcp/defi/swap", "POST", { fromToken, toToken, amount, taker: localWallet.address, slippagePercentage: slippageLimit / 100 }, "swap_tokens");
107
110
  if (!result.success)
108
111
  return { content: [{ type: "text", text: `Estimate failed: ${result.error}` }], isError: true };
109
112
  const q = result.quote;
@@ -163,17 +166,25 @@ async function handleDefiTool(name, args) {
163
166
  }
164
167
  const txHash = await (0, wallet_js_1.signAndBroadcast)(wallet, q);
165
168
  const buyAmountHuman = formatTokenAmount(q.buyAmount, q.buyToken ?? toToken);
169
+ const receipt = await (0, wallet_js_1.waitForReceipt)(txHash);
170
+ const header = !receipt.mined
171
+ ? `⏳ Broadcast, not yet confirmed within the wait window.`
172
+ : receipt.ok
173
+ ? `✅ Swap confirmed on-chain (block ${receipt.blockNumber}).`
174
+ : `❌ Transaction REVERTED - no swap occurred (gas was still spent).`;
166
175
  return {
167
176
  content: [{
168
177
  type: "text",
169
178
  text: [
170
- `✅ Swap executed!`,
171
- `${amount} ${fromToken.toUpperCase()} → ${buyAmountHuman} ${q.buyToken}`,
179
+ header,
180
+ `${amount} ${fromToken.toUpperCase()} → ${receipt.mined && receipt.ok ? buyAmountHuman : "(quoted, not received)"} ${q.buyToken}`,
172
181
  `Slippage cap: ${slippageLimit}% · Price impact: ${impactPct.toFixed(3)}%`,
173
182
  `Tx Hash: \`${txHash}\``,
174
183
  `https://basescan.org/tx/${txHash}`,
175
- ].join("\n"),
184
+ !receipt.mined ? `Check the link above before telling the user this succeeded or failed.` : "",
185
+ ].filter(Boolean).join("\n"),
176
186
  }],
187
+ isError: receipt.mined && !receipt.ok,
177
188
  };
178
189
  }
179
190
  case "send_token": {
@@ -186,41 +197,31 @@ async function handleDefiTool(name, args) {
186
197
  if (!result.success)
187
198
  return { content: [{ type: "text", text: `Send failed: ${result.error}` }], isError: true };
188
199
  const txHash = await (0, wallet_js_1.signAndBroadcast)(wallet, result.txData);
200
+ const receipt = await (0, wallet_js_1.waitForReceipt)(txHash);
201
+ const header = !receipt.mined
202
+ ? `⏳ Broadcast, not yet confirmed within the wait window.`
203
+ : receipt.ok
204
+ ? `✅ Sent - confirmed on-chain (block ${receipt.blockNumber}).`
205
+ : `❌ Transaction REVERTED - funds were NOT sent (gas was still spent).`;
189
206
  return {
190
207
  content: [{
191
208
  type: "text",
192
- text: [`✅ Sent!`, `${amount} ${token.toUpperCase()} → \`${toAddress}\``, `Tx Hash: \`${txHash}\``, `https://basescan.org/tx/${txHash}`].join("\n"),
209
+ text: [
210
+ header,
211
+ `${amount} ${token.toUpperCase()} → \`${toAddress}\``,
212
+ `Tx Hash: \`${txHash}\``,
213
+ `https://basescan.org/tx/${txHash}`,
214
+ !receipt.mined ? `Check the link above before telling the user this succeeded or failed.` : "",
215
+ ].filter(Boolean).join("\n"),
193
216
  }],
217
+ isError: receipt.mined && !receipt.ok,
194
218
  };
195
219
  }
196
- case "analyze_wallet": {
197
- const parsed = AnalyzeWalletSchema.safeParse(args);
198
- if (!parsed.success)
199
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
200
- const { address, label } = parsed.data;
201
- const data = await (0, convex_js_1.callConvex)("/wallet/analyze", "POST", { address, label }, "analyze_wallet");
202
- if (data.error)
203
- return { content: [{ type: "text", text: `Wallet analysis failed: ${data.error}` }], isError: true };
204
- const total = (data.totalUsd ?? 0).toFixed(2);
205
- const walletLabel = label ? ` - ${label}` : "";
206
- const topHoldings = (data.holdings ?? [])
207
- .slice(0, 8)
208
- .map(h => `• **${h.token}**: $${(h.valueUsd ?? 0).toFixed(2)}${h.pct != null ? ` (${h.pct}%)` : ""}`)
209
- .join("\n");
210
- const profileLine = data.profile ? `**Profile:** ${data.profile}\n` : "";
211
- const header = [
212
- `**Wallet Analysis**${walletLabel}`,
213
- `\`${address}\``,
214
- `**Portfolio value:** $${total}`,
215
- ``,
216
- profileLine,
217
- `**Holdings:**`,
218
- topHoldings || "No token holdings found.",
219
- ``,
220
- ].join("\n");
221
- const body = data.analysis ?? (data.analysisError ? `*AI analysis unavailable: ${data.analysisError}*` : "*AI analysis not available*");
222
- return { content: [{ type: "text", text: header + body }] };
223
- }
220
+ // analyze_wallet was removed - it called POST /wallet/analyze, which was
221
+ // never registered in app/convex/http.ts (only a dangling section-header
222
+ // comment exists there). The tool was never in the exported DEFI_TOOLS
223
+ // list either, so this case was already unreachable dead code - no Tool
224
+ // registers the name "analyze_wallet" for the MCP dispatcher to route to.
224
225
  case "get_defi_yields": {
225
226
  const parsed = DefiYieldsSchema.safeParse(args ?? {});
226
227
  if (!parsed.success)
@@ -11,8 +11,13 @@
11
11
  // and is a stronger analyst than anything this module could embed.
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.EQUITY_TOOLS = void 0;
14
+ exports.buildStockFundamentals = buildStockFundamentals;
14
15
  exports.handleEquityTool = handleEquityTool;
15
16
  const zod_1 = require("zod");
17
+ // ── Structured output builder (schema in output-schemas.ts) ─────────────────
18
+ function buildStockFundamentals(ticker, companyName, cik, quote, quarterly, annual) {
19
+ return { ticker, companyName, cik, quote, quarterly, annual };
20
+ }
16
21
  const SEC_UA = "Finch MCP research (contact: support@finchagentic.com)";
17
22
  const SEC_TICKERS = "https://www.sec.gov/files/company_tickers.json";
18
23
  const SEC_CONCEPT = (cik, tag) => `https://data.sec.gov/api/xbrl/companyconcept/CIK${cik}/us-gaap/${tag}.json`;
@@ -360,5 +365,8 @@ async function handleEquityTool(name, args) {
360
365
  // Issuers tag the same concept differently, so naming the tag behind each
361
366
  // series makes the figures checkable against EDGAR rather than trusted.
362
367
  `_XBRL concepts used: ${usedTags.join(" · ")}_`, ``, `_Figures are as filed with the SEC. Not investment advice._`);
363
- return { content: [{ type: "text", text: lines.join("\n") }] };
368
+ return {
369
+ content: [{ type: "text", text: lines.join("\n") }],
370
+ structuredContent: buildStockFundamentals(ticker, company.name ?? null, company.cik ?? null, quote, quarterly, annual),
371
+ };
364
372
  }
@@ -11,11 +11,11 @@ const signal_gate_js_1 = require("../signal-gate.js");
11
11
  exports.INSIGHT_TOOLS = [
12
12
  {
13
13
  name: "ask_finch",
14
- description: "Ask Noel anything - analysis, opinions, explanations, strategy, or ideas. Noel loads your saved memory to personalize every answer. Use for: research questions, content ideas, code explanations, decision-making, DeFi analysis, trade ideas, or just thinking out loud. Pass previous messages to continue a conversation across tool calls.",
14
+ description: "Ask Finch anything - analysis, opinions, explanations, strategy, or ideas. Finch loads your saved memory to personalize every answer. Use for: research questions, content ideas, code explanations, decision-making, DeFi analysis, trade ideas, or just thinking out loud. Pass previous messages to continue a conversation across tool calls. If YOU are already a reasoning model (Claude, GPT, etc. calling this via MCP) and other tools already gave you the data you need, just answer directly instead of calling this - it runs a separate LLM call and won't tell you anything you can't already work out yourself from that data.",
15
15
  inputSchema: {
16
16
  type: "object",
17
17
  properties: {
18
- question: { type: "string", description: "Your question or request for Noel" },
18
+ question: { type: "string", description: "Your question or request for Finch" },
19
19
  messages: {
20
20
  type: "array",
21
21
  description: "Previous conversation messages for context (optional)",
@@ -63,7 +63,7 @@ exports.INSIGHT_TOOLS = [
63
63
  },
64
64
  },
65
65
  ];
66
- const AskNoelSchema = zod_1.z.object({
66
+ const AskFinchSchema = zod_1.z.object({
67
67
  question: zod_1.z.string().min(1),
68
68
  messages: zod_1.z.array(zod_1.z.object({ role: zod_1.z.enum(["user", "assistant"]), content: zod_1.z.string() })).optional(),
69
69
  });
@@ -78,12 +78,12 @@ const TradePlanSchema = zod_1.z.object({
78
78
  riskTolerance: zod_1.z.enum(["conservative", "moderate", "aggressive"]).optional(),
79
79
  timeframe: zod_1.z.string().optional(),
80
80
  });
81
- const NOEL_BASE_PROMPT = `You are Noel, the core intelligence of the Finch runtime - the persistent state layer for AI assistants. You are direct, sharp, and thorough. You have access to memory that accumulates, vaults that version knowledge, agents that keep running between sessions, workflows that execute on schedule, plus execution domains: web research, market intelligence, code, and DeFi on Base. When asked anything, give your honest read backed by real reasoning. No filler, no disclaimers.
81
+ const FINCH_BASE_PROMPT = `You are Finch, the core intelligence of the Finch runtime - the persistent state layer for AI assistants. You are direct, sharp, and thorough. You have access to memory that accumulates, vaults that version knowledge, agents that keep running between sessions, workflows that execute on schedule, plus execution domains: web research, market intelligence, code, and DeFi on Base. When asked anything, give your honest read backed by real reasoning. No filler, no disclaimers.
82
82
 
83
83
  When the user's vault or memory contains relevant prior research, build on it explicitly rather than starting from scratch. Reference vault entries by title when you cite them.`;
84
84
  async function searchVault(question, limit = 3) {
85
85
  try {
86
- const data = await (0, convex_js_1.callConvex)("/vault/search", "POST", { query: question, limit }, "ask_finch_vault");
86
+ const data = await (0, convex_js_1.callConvex)("/vault/search", "POST", { query: question, limit }, "vault_search");
87
87
  const entries = data?.entries ?? data?.results ?? [];
88
88
  return Array.isArray(entries) ? entries.slice(0, limit) : [];
89
89
  }
@@ -93,7 +93,7 @@ async function searchVault(question, limit = 3) {
93
93
  }
94
94
  async function fetchProfileContext() {
95
95
  try {
96
- const data = await (0, convex_js_1.callConvex)("/vault/profile-context?maxChars=3000", "GET", undefined, "profile_context");
96
+ const data = await (0, convex_js_1.callConvex)("/vault/profile-context?maxChars=3000", "GET", undefined, "vault_read");
97
97
  return (data?.context ?? "").trim();
98
98
  }
99
99
  catch {
@@ -138,8 +138,8 @@ async function buildSystemPrompt(question) {
138
138
  blocks.push(`<user_vault>\nThe user already has these prior artifacts on closely related topics - build on them, don't repeat them:\n${vaultBlock}\n</user_vault>`);
139
139
  }
140
140
  const prompt = blocks.length === 0
141
- ? NOEL_BASE_PROMPT
142
- : `${NOEL_BASE_PROMPT}\n\n${blocks.join("\n\n")}`;
141
+ ? FINCH_BASE_PROMPT
142
+ : `${FINCH_BASE_PROMPT}\n\n${blocks.join("\n\n")}`;
143
143
  return { prompt, meta };
144
144
  }
145
145
  function formatContextHeader(meta) {
@@ -391,7 +391,7 @@ function spreadNote(priceData) {
391
391
  }
392
392
  async function handleInsightTool(name, args) {
393
393
  if (name === "ask_finch") {
394
- const parsed = AskNoelSchema.safeParse(args);
394
+ const parsed = AskFinchSchema.safeParse(args);
395
395
  if (!parsed.success)
396
396
  return { content: [{ type: "text", text: `Invalid input: question ${parsed.error.issues[0].message}` }], isError: true };
397
397
  const { question, messages = [] } = parsed.data;
@@ -419,25 +419,21 @@ async function handleInsightTool(name, args) {
419
419
  systemPrompt += `\n\nCRITICAL: User is asking about a current price but no live data was retrieved. Reply honestly: "I don't have a live price quote right now - check CoinGecko or DexScreener directly." DO NOT state any specific price number. DO NOT recall a price from training data.`;
420
420
  }
421
421
  }
422
- let answer = null;
423
- if (process.env.BANKR_API_KEY || process.env.ANTHROPIC_API_KEY) {
424
- try {
425
- const history = messages.map(m => ({ role: m.role, content: m.content }));
426
- answer = await (0, llm_js_1.callLLM)(systemPrompt, question, 1024, history);
427
- }
428
- catch {
429
- // fall through to Convex backend
430
- }
431
- }
432
- if (!answer) {
433
- const data = await (0, convex_js_1.callConvex)("/mcp/chat", "POST", {
434
- question,
435
- agentId: "noel-default",
436
- messages,
437
- systemPrompt,
438
- }, "ask_finch");
439
- answer = data.answer ?? JSON.stringify(data);
422
+ if (!(0, llm_js_1.hasDirectLLMKey)()) {
423
+ return {
424
+ content: [{
425
+ type: "text",
426
+ text: "ask_finch needs its own LLM key to reason server-side - set one of " +
427
+ "BANKR_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY, or GROK_API_KEY as an " +
428
+ "environment variable, then retry. If YOU (the calling model) already have " +
429
+ "the context needed to answer this, just answer directly instead - that's " +
430
+ "usually faster and doesn't need a key at all.",
431
+ }],
432
+ isError: true,
433
+ };
440
434
  }
435
+ const history = messages.map(m => ({ role: m.role, content: m.content }));
436
+ const answer = await (0, llm_js_1.callLLM)(systemPrompt, question, 1024, history);
441
437
  // Signal gate - if the model returned thin/meta output, flag it so the
442
438
  // user knows to retry with more specific framing or use deep_research.
443
439
  const signal = (0, signal_gate_js_1.checkSignal)(answer);
@@ -485,7 +481,7 @@ async function handleInsightTool(name, args) {
485
481
  // thesis is model work — and the caller is a model that can also weigh the
486
482
  // user's stated context. Hand over grounded data plus the structure.
487
483
  const suggest = process.env.TRIGGER_SECRET_KEY
488
- ? `\n\n---\n💡 Use \`create_monitor\` for scheduled briefings on ${token.toUpperCase()}.`
484
+ ? `\n\n---\n💡 Use \`schedule_research\` for scheduled briefings on ${token.toUpperCase()}.`
489
485
  : "";
490
486
  return {
491
487
  content: [{
@@ -579,7 +575,7 @@ async function handleInsightTool(name, args) {
579
575
  ]
580
576
  : [`**Max position (${riskTolerance}):** ${band.label} — pass \`portfolioSize\` for USD figures.`];
581
577
  const suggest = process.env.TRIGGER_SECRET_KEY
582
- ? `\n\n---\n💡 Use \`create_monitor\` for scheduled briefings on ${token.toUpperCase()}.`
578
+ ? `\n\n---\n💡 Use \`schedule_research\` for scheduled briefings on ${token.toUpperCase()}.`
583
579
  : "";
584
580
  return {
585
581
  content: [{