@finchagentic/mcp 4.1.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());
@@ -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(),
@@ -214,34 +217,11 @@ async function handleDefiTool(name, args) {
214
217
  isError: receipt.mined && !receipt.ok,
215
218
  };
216
219
  }
217
- case "analyze_wallet": {
218
- const parsed = AnalyzeWalletSchema.safeParse(args);
219
- if (!parsed.success)
220
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
221
- const { address, label } = parsed.data;
222
- const data = await (0, convex_js_1.callConvex)("/wallet/analyze", "POST", { address, label }, "analyze_wallet");
223
- if (data.error)
224
- return { content: [{ type: "text", text: `Wallet analysis failed: ${data.error}` }], isError: true };
225
- const total = (data.totalUsd ?? 0).toFixed(2);
226
- const walletLabel = label ? ` - ${label}` : "";
227
- const topHoldings = (data.holdings ?? [])
228
- .slice(0, 8)
229
- .map(h => `• **${h.token}**: $${(h.valueUsd ?? 0).toFixed(2)}${h.pct != null ? ` (${h.pct}%)` : ""}`)
230
- .join("\n");
231
- const profileLine = data.profile ? `**Profile:** ${data.profile}\n` : "";
232
- const header = [
233
- `**Wallet Analysis**${walletLabel}`,
234
- `\`${address}\``,
235
- `**Portfolio value:** $${total}`,
236
- ``,
237
- profileLine,
238
- `**Holdings:**`,
239
- topHoldings || "No token holdings found.",
240
- ``,
241
- ].join("\n");
242
- const body = data.analysis ?? (data.analysisError ? `*AI analysis unavailable: ${data.analysisError}*` : "*AI analysis not available*");
243
- return { content: [{ type: "text", text: header + body }] };
244
- }
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.
245
225
  case "get_defi_yields": {
246
226
  const parsed = DefiYieldsSchema.safeParse(args ?? {});
247
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
  }
@@ -481,7 +481,7 @@ async function handleInsightTool(name, args) {
481
481
  // thesis is model work — and the caller is a model that can also weigh the
482
482
  // user's stated context. Hand over grounded data plus the structure.
483
483
  const suggest = process.env.TRIGGER_SECRET_KEY
484
- ? `\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()}.`
485
485
  : "";
486
486
  return {
487
487
  content: [{
@@ -575,7 +575,7 @@ async function handleInsightTool(name, args) {
575
575
  ]
576
576
  : [`**Max position (${riskTolerance}):** ${band.label} — pass \`portfolioSize\` for USD figures.`];
577
577
  const suggest = process.env.TRIGGER_SECRET_KEY
578
- ? `\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()}.`
579
579
  : "";
580
580
  return {
581
581
  content: [{
@@ -47,6 +47,7 @@ const zod_1 = require("zod");
47
47
  const crypto = __importStar(require("crypto"));
48
48
  const convex_js_1 = require("../convex.js");
49
49
  const public_url_js_1 = require("../public-url.js");
50
+ const local_vault_js_1 = require("../local-vault.js");
50
51
  const local_memory_js_1 = require("../local-memory.js");
51
52
  // memory_extract and memory_consolidate used to run their own LLM calls here
52
53
  // (and a matching pair of Convex routes did the same server-side). Both are now
@@ -89,6 +90,18 @@ function lookupRecentHash(hash) {
89
90
  }
90
91
  return hit;
91
92
  }
93
+ // memory_delete must purge this too - otherwise a deleted memory's hash stays
94
+ // cached (up to an hour) and re-adding the exact same content within that
95
+ // window gets silently skipped as a "duplicate" of an id that no longer
96
+ // exists, when there is no longer any real duplicate to skip.
97
+ function forgetRecentHashById(id) {
98
+ for (const [hash, entry] of recentHashCache) {
99
+ if (entry.id === id) {
100
+ recentHashCache.delete(hash);
101
+ break;
102
+ }
103
+ }
104
+ }
92
105
  // Two-tier dedup lookup: in-process cache first (catches same-session
93
106
  // dupes during eventual-consistency window), then a list call (catches
94
107
  // cross-session dupes once they've been indexed). Returns null on any
@@ -410,26 +423,6 @@ exports.MEMORY_TOOLS = [
410
423
  required: [],
411
424
  },
412
425
  },
413
- {
414
- name: "memory_publish",
415
- description: "IRREVERSIBLE, PUBLIC. Publish a memory snippet to the Memory Marketplace — visible to " +
416
- "ALL Finch users at /memory-marketplace. Reversible with vault_unpublish, but only for " +
417
- "future discovery — anyone who already read it keeps what they saw. " +
418
- "Requires confirm: true. Never call this on the user's behalf without them explicitly asking " +
419
- "to publish; re-read the content for anything private (keys, addresses, personal details) first. " +
420
- "Saved as a public vault entry (type=memory).",
421
- inputSchema: {
422
- type: "object",
423
- properties: {
424
- title: { type: "string", description: "Short title for the memory (shown publicly in the marketplace)" },
425
- content: { type: "string", description: "The memory content to share — this becomes PUBLIC" },
426
- tags: { type: "array", items: { type: "string" }, description: "Optional tags (e.g. ['DeFi', 'Base', 'research'])" },
427
- authorName: { type: "string", description: "Public display name. Defaults to \"Anonymous\" — do NOT pass a wallet address unless the user asks to be identified." },
428
- confirm: { type: "boolean", description: "Must be true to publish. Guards against accidental public disclosure." },
429
- },
430
- required: ["title", "content", "confirm"],
431
- },
432
- },
433
426
  {
434
427
  name: "memory_consolidate",
435
428
  description: "Clean up fragmented knowledge after heavy research sessions. Two-pass, no API key needed. " +
@@ -709,7 +702,7 @@ async function handleMemoryTool(name, args) {
709
702
  content: [{
710
703
  type: "text",
711
704
  text: [
712
- `🧠 **Finch Semantic Memory**`,
705
+ `🧠 **Finch Memory**`,
713
706
  ``,
714
707
  `Space: \`${space}\``,
715
708
  `Total memories: **${total}**`,
@@ -720,7 +713,7 @@ async function handleMemoryTool(name, args) {
720
713
  `• memory_add (URL indexing) - ✅`,
721
714
  `• Google Drive / Gmail / Notion - connect at finchagentic.com`,
722
715
  ``,
723
- `**Capabilities:** Semantic search · Vector context · 81.6% LongMemEval`,
716
+ `**Capabilities:** Full-text (keyword) search with 90-day time-decay ranking - not embeddings, no vector search.`,
724
717
  ].join("\n"),
725
718
  }],
726
719
  structuredContent: buildMemoryProfile(data),
@@ -785,6 +778,7 @@ async function handleMemoryTool(name, args) {
785
778
  if (data?.error)
786
779
  return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
787
780
  }
781
+ forgetRecentHashById(parsed.data.id);
788
782
  return { content: [{ type: "text", text: `🗑️ Memory deleted: \`${parsed.data.id}\`` }] };
789
783
  }
790
784
  case "memory_insight": {
@@ -795,10 +789,15 @@ async function handleMemoryTool(name, args) {
795
789
  const memLimit = depth === "deep" ? 15 : depth === "quick" ? 5 : 8;
796
790
  // v3.25.1: hybrid retrieval for memory side (was semantic-only) so the
797
791
  // intelligence report surfaces exact-token matches alongside meaning
798
- // matches. Vault side runs in parallel as before.
792
+ // matches. Vault side runs in parallel as before - but only against
793
+ // Convex when the vault isn't local; a local vault's topic string must
794
+ // never leave the machine, and this never checked that before.
795
+ const localVault = (0, local_vault_js_1.getLocalVaultConfig)();
799
796
  const [memResults, vaultData] = await Promise.all([
800
797
  hybridMemorySearch(topic, memLimit),
801
- (0, convex_js_1.callConvex)(`/vault/search?q=${encodeURIComponent(topic)}&limit=6`, "GET", undefined, "vault_search").catch(() => ({ results: [] })),
798
+ localVault
799
+ ? Promise.resolve((0, local_vault_js_1.localVaultSearch)(localVault, topic, { limit: 6 }))
800
+ : (0, convex_js_1.callConvex)(`/vault/search?q=${encodeURIComponent(topic)}&limit=6`, "GET", undefined, "vault_search").catch(() => ({ results: [] })),
802
801
  ]);
803
802
  const vaultResults = vaultData.results ?? [];
804
803
  const total = memResults.length + vaultResults.length;
@@ -955,7 +954,11 @@ async function handleMemoryTool(name, args) {
955
954
  };
956
955
  }
957
956
  // ── PASS 1: fetch every memory on the topic, numbered ───────────────
958
- const rows = await searchSupermemory(topic, limit);
957
+ // Uses the same RRF-fused semantic+lexical retrieval as memory_search/
958
+ // memory_context - a plain single-source searchSupermemory() call here
959
+ // used to mean consolidation could miss memories only the lexical/BM25
960
+ // side would surface, while still claiming an exhaustive "N memories".
961
+ const rows = await hybridMemorySearch(topic, limit);
959
962
  if (rows.length === 0) {
960
963
  return { content: [{ type: "text", text: `No memories found for "${topic}" to consolidate.` }], isError: true };
961
964
  }
@@ -989,61 +992,22 @@ async function handleMemoryTool(name, args) {
989
992
  }],
990
993
  };
991
994
  }
992
- case "memory_publish": {
993
- const { title, content, tags, authorName, confirm } = args;
994
- if (!title || !content)
995
- return { content: [{ type: "text", text: "title and content are required" }], isError: true };
996
- if (confirm !== true) {
997
- return {
998
- content: [{
999
- type: "text",
1000
- text: "Refusing to publish: this makes the content **public to all Finch users**. " +
1001
- "It can be hidden again with `vault_unpublish`, but not un-read. Review the content " +
1002
- "for keys, addresses and personal details, then pass `confirm: true`.",
1003
- }],
1004
- isError: true,
1005
- };
1006
- }
1007
- if (authorName && /^0x[a-fA-F0-9]{40}$/.test(authorName.trim())) {
1008
- return {
1009
- content: [{
1010
- type: "text",
1011
- text: "Refusing to publish with a wallet address as the author name — that permanently " +
1012
- "links your on-chain identity to this public entry. Use a handle, or omit authorName " +
1013
- "to publish as \"Anonymous\".",
1014
- }],
1015
- isError: true,
1016
- };
1017
- }
1018
- const data = await (0, convex_js_1.callConvex)("/vault/save", "POST", {
1019
- type: "memory",
1020
- title,
1021
- content,
1022
- tags: tags ?? [],
1023
- isPublic: true,
1024
- authorName: authorName ?? "Anonymous",
1025
- commitMsg: "published to marketplace",
1026
- }, "vault_save");
1027
- if (data.error)
1028
- return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
1029
- return {
1030
- content: [{
1031
- type: "text",
1032
- text: [
1033
- `🧠 **Memory Published**`,
1034
- ``,
1035
- `**Title:** ${title}`,
1036
- `**Key:** \`${data.key}\``,
1037
- `**Version:** ${data.version ?? 1}`,
1038
- ``,
1039
- `Now visible at the Memory Marketplace in the Finch app.`,
1040
- ``,
1041
- `Make it private again: \`vault_unpublish key: "${data.key}"\``,
1042
- `That stops future discovery. Anyone who already read it keeps what they saw.`,
1043
- ].join("\n"),
1044
- }],
1045
- };
1046
- }
995
+ // memory_publish was removed - it was broken two levels deep. The tool
996
+ // called POST /vault/save with isPublic/authorName fields that the
997
+ // handler silently dropped (only type/title/content/key/contentType/
998
+ // agentId/tags/commitMsg/metadata are forwarded to vault.saveEntry - see
999
+ // app/convex/http.ts), so `published` was never actually set true; the
1000
+ // real publishEntry mutation (POST /vault/publish) existed but this tool
1001
+ // never called it. And even a correctly-wired publish would have done
1002
+ // nothing observable: nothing anywhere reads the `published` field for
1003
+ // cross-user browsing - no /vault/community route (dangling comment
1004
+ // only, same pattern as the other removed routes), no backend query for
1005
+ // it, no "Memory Marketplace" page in app/src. The tool asked users to
1006
+ // accept an "IRREVERSIBLE, PUBLIC" risk for a marketplace that doesn't
1007
+ // exist at any layer. Re-add only alongside building the actual
1008
+ // discovery path: a community-browse query + route + UI that reads
1009
+ // `published: true` entries. vault_unpublish is left in place - it's a
1010
+ // correct, harmless no-op until then.
1047
1011
  default:
1048
1012
  return null;
1049
1013
  }
@@ -22,8 +22,8 @@ const MONITOR_INPUT_SCHEMA = {
22
22
  exports.MONITOR_TOOLS = [
23
23
  {
24
24
  name: "schedule_research",
25
- description: "Schedule recurring autonomous research on any topic - runs on a cron schedule, saves findings to vault, " +
26
- "and sends a Telegram notification. The agent runs completely on its own with no prompting needed. " +
25
+ description: "Schedule recurring autonomous research on any topic - runs on a cron schedule, saves findings to vault. " +
26
+ "The agent runs completely on its own with no prompting needed. " +
27
27
  "Requires TRIGGER_SECRET_KEY env var (trigger.dev). " +
28
28
  "Examples: daily morning briefing, weekly competitor analysis, hourly price alerts, monthly industry report.",
29
29
  inputSchema: MONITOR_INPUT_SCHEMA,
@@ -103,7 +103,7 @@ function buildMonitorList(schedules, configs = {}) {
103
103
  };
104
104
  }
105
105
  async function handleMonitorTool(name, args) {
106
- if (name === "schedule_research" || name === "create_monitor") {
106
+ if (name === "schedule_research") {
107
107
  const parsed = CreateSchema.safeParse(args);
108
108
  if (!parsed.success)
109
109
  return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
@@ -192,7 +192,7 @@ async function handleMonitorTool(name, args) {
192
192
  key: `monitor-config/${externalId}`,
193
193
  agentId: "os",
194
194
  tags: ["monitor-config"],
195
- commitMsg: "create_monitor config",
195
+ commitMsg: "schedule_research config",
196
196
  }, "vault_save");
197
197
  }
198
198
  catch {
@@ -210,7 +210,7 @@ async function handleMonitorTool(name, args) {
210
210
  data.nextRun ? `⏭️ Next run: ${new Date(data.nextRun).toUTCString()}` : "",
211
211
  ``,
212
212
  configSaved
213
- ? `The agent will research "${topic}" on schedule, save findings to vault, and send a Telegram notification if configured.`
213
+ ? `The agent will research "${topic}" on schedule and save findings to vault.`
214
214
  : `⚠️ Monitor schedule created but config save failed - the agent may use a default topic on first run. Try \`cancel_monitor\` and recreate.`,
215
215
  `Use \`list_monitors\` to see all active monitors.`,
216
216
  ].filter(Boolean).join("\n"),
@@ -240,7 +240,7 @@ async function handleMonitorTool(name, args) {
240
240
  return {
241
241
  content: [{
242
242
  type: "text",
243
- text: `No active monitors.\n\nUse \`create_monitor\` to set up an autonomous agent that runs on a schedule.`,
243
+ text: `No active monitors.\n\nUse \`schedule_research\` to set up an autonomous agent that runs on a schedule.`,
244
244
  }],
245
245
  structuredContent: buildMonitorList([]),
246
246
  };
package/dist/tools/os.js CHANGED
@@ -138,7 +138,6 @@ async function handleOsTool(name, args) {
138
138
  "FIRECRAWL_API_KEY": !!process.env.FIRECRAWL_API_KEY,
139
139
  "FINCH_SESSION_TOKEN": !!process.env.FINCH_SESSION_TOKEN,
140
140
  "FINCH_API_KEY": !!process.env.FINCH_API_KEY,
141
- "TELEGRAM_BOT_TOKEN": !!process.env.TELEGRAM_BOT_TOKEN,
142
141
  };
143
142
  const localMemCfg = (0, local_memory_js_1.getLocalMemoryConfig)();
144
143
  const pingLocalMemory = async () => {
@@ -245,8 +245,8 @@ async function handlePacket(toolName, args) {
245
245
  }
246
246
  if (toolName === "packet_share") {
247
247
  const { name, authorName, confirm } = args;
248
- // Same bar as memory_publish and vault_delete: an action the user cannot
249
- // fully take back needs an explicit second step, not a single call.
248
+ // Same bar as vault_delete: an action the user cannot fully take back
249
+ // needs an explicit second step, not a single call.
250
250
  if (confirm !== true) {
251
251
  return {
252
252
  content: [{
@@ -14,6 +14,7 @@ exports.dexTokenBest = dexTokenBest;
14
14
  exports.resolveTokenSmart = resolveTokenSmart;
15
15
  exports.buildRhAnalysis = buildRhAnalysis;
16
16
  exports.buildRhStocksList = buildRhStocksList;
17
+ exports.rhProviderAsync = rhProviderAsync;
17
18
  exports.rhErc20Balance = rhErc20Balance;
18
19
  exports.rhPriceUsd = rhPriceUsd;
19
20
  exports.buildRhSafetyStructured = buildRhSafetyStructured;
@@ -990,7 +991,16 @@ async function quoteRh(args) {
990
991
  throw new Error("RH swaps route ETH ↔ token. Buy a token with ETH, or sell a token for ETH.");
991
992
  }
992
993
  const sellAmount = parseHumanToWei(args.amount, from.decimals);
993
- const slippageBps = Math.round((args.maxSlippagePct ?? 2.0) * 100);
994
+ const slippagePct = args.maxSlippagePct ?? 2.0;
995
+ // Same bound defi.ts's SwapSchema already enforces for Base swaps
996
+ // (.positive().max(50)) - this file had no equivalent check anywhere, so a
997
+ // negative or absurd value flowed straight through to the backend as
998
+ // slippageBps, both for direct rh_mcp_swap calls and for every unattended
999
+ // rh_orders_tick execution of a DCA/bracket order created with a bad value.
1000
+ if (!(slippagePct > 0) || slippagePct > 50) {
1001
+ throw new Error(`maxSlippagePct must be greater than 0 and at most 50 (got ${args.maxSlippagePct}).`);
1002
+ }
1003
+ const slippageBps = Math.round(slippagePct * 100);
994
1004
  const result = await (0, convex_js_1.callConvex)("/mcp/rh/quote", "POST", {
995
1005
  sellToken: from.address,
996
1006
  buyToken: to.address,
@@ -999,7 +1009,7 @@ async function quoteRh(args) {
999
1009
  slippageBps,
1000
1010
  fromSymbol: from.symbol,
1001
1011
  toSymbol: to.symbol,
1002
- }, "rh_mcp_estimate");
1012
+ }, args.toolName ?? "rh_mcp_estimate");
1003
1013
  if (result.error)
1004
1014
  throw new Error(result.error);
1005
1015
  return { ...result, from, to, sellAmount, slippageBps };
@@ -1269,6 +1279,7 @@ async function handleRhMcpTool(name, args) {
1269
1279
  amount: String(a.amount),
1270
1280
  maxSlippagePct: a.maxSlippagePct,
1271
1281
  taker: wallet.address,
1282
+ toolName: "rh_mcp_swap",
1272
1283
  });
1273
1284
  const quote = q.quote ?? q;
1274
1285
  const tx = quote.transaction ?? {