@finchagentic/mcp 4.1.0 → 4.4.1

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 (44) hide show
  1. package/README.md +19 -15
  2. package/dist/agent-loop.js +75 -72
  3. package/dist/annotations.js +23 -14
  4. package/dist/convex.js +15 -18
  5. package/dist/index.js +14 -12
  6. package/dist/llm.js +12 -1
  7. package/dist/local-memory-file.js +14 -1
  8. package/dist/local-memory.js +13 -0
  9. package/dist/output-schemas.js +71 -17
  10. package/dist/project.js +36 -0
  11. package/dist/resources.js +6 -11
  12. package/dist/server.js +35 -13
  13. package/dist/token-gate.js +2 -2
  14. package/dist/tool-filter.js +14 -5
  15. package/dist/tools/agents.js +106 -394
  16. package/dist/tools/automation.js +42 -6
  17. package/dist/tools/base-mcp.js +3 -15
  18. package/dist/tools/base.js +34 -20
  19. package/dist/tools/coder.js +1 -1
  20. package/dist/tools/deep-research.js +1 -1
  21. package/dist/tools/defi.js +14 -34
  22. package/dist/tools/equity.js +10 -2
  23. package/dist/tools/events.js +1 -1
  24. package/dist/tools/github.js +51 -1
  25. package/dist/tools/insider.js +1 -1
  26. package/dist/tools/insight.js +4 -4
  27. package/dist/tools/market.js +5 -5
  28. package/dist/tools/memory.js +52 -88
  29. package/dist/tools/miroshark.js +8 -1
  30. package/dist/tools/monitor.js +8 -8
  31. package/dist/tools/os.js +9 -4
  32. package/dist/tools/packets.js +2 -2
  33. package/dist/tools/research-chain.js +1 -1
  34. package/dist/tools/research-compare.js +1 -1
  35. package/dist/tools/research.js +2 -2
  36. package/dist/tools/rh-bridge.js +1 -1
  37. package/dist/tools/rh-mcp.js +29 -4
  38. package/dist/tools/rh-orders.js +201 -123
  39. package/dist/tools/scanner.js +33 -3
  40. package/dist/tools/stake.js +369 -0
  41. package/dist/tools/vault.js +294 -40
  42. package/dist/wallet.js +130 -19
  43. package/package.json +4 -5
  44. package/dist/tools/framework.js +0 -150
@@ -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. " +
@@ -526,7 +519,7 @@ async function handleMemoryTool(name, args) {
526
519
  case "memory_add": {
527
520
  const parsed = AddSchema.safeParse(args);
528
521
  if (!parsed.success)
529
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
522
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
530
523
  const { content, title, tags, sourceUrl, force } = parsed.data;
531
524
  // `sourceUrl` is not stored, it is fetched and indexed — by the local
532
525
  // memory server on the user's own machine when local mode is on. An
@@ -587,7 +580,7 @@ async function handleMemoryTool(name, args) {
587
580
  case "memory_search": {
588
581
  const parsed = SearchSchema.safeParse(args);
589
582
  if (!parsed.success)
590
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
583
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
591
584
  const { query, limit = 10 } = parsed.data;
592
585
  // Over-fetch so post-decay ranking still has enough material.
593
586
  const overfetch = Math.min(50, Math.max(limit * 2, 20));
@@ -669,7 +662,7 @@ async function handleMemoryTool(name, args) {
669
662
  case "memory_context": {
670
663
  const parsed = ContextSchema.safeParse(args);
671
664
  if (!parsed.success)
672
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
665
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
673
666
  const { topic, limit = 8 } = parsed.data;
674
667
  // v3.25.1: use hybrid retrieval so context loading picks up exact-token
675
668
  // matches (env var names, model IDs, contract addresses) that semantic-
@@ -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),
@@ -729,7 +722,7 @@ async function handleMemoryTool(name, args) {
729
722
  case "memory_list": {
730
723
  const parsed = ListSchema.safeParse(args ?? {});
731
724
  if (!parsed.success)
732
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
725
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
733
726
  const { limit = 20, tag } = parsed.data;
734
727
  const localList = (0, local_memory_js_1.getLocalMemoryConfig)();
735
728
  let results;
@@ -760,7 +753,7 @@ async function handleMemoryTool(name, args) {
760
753
  case "memory_delete": {
761
754
  const parsed = DeleteMemSchema.safeParse(args);
762
755
  if (!parsed.success)
763
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
756
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
764
757
  if (args?.confirm !== true) {
765
758
  return {
766
759
  content: [{
@@ -785,20 +778,26 @@ 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": {
791
785
  const parsed = InsightSchema.safeParse(args);
792
786
  if (!parsed.success)
793
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
787
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
794
788
  const { topic, depth = "standard" } = parsed.data;
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;
@@ -873,7 +872,7 @@ async function handleMemoryTool(name, args) {
873
872
  case "memory_extract": {
874
873
  const parsed = ExtractSchema.safeParse(args);
875
874
  if (!parsed.success)
876
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
875
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
877
876
  const { text, facts, source = "extract" } = parsed.data;
878
877
  // ── PASS 1: hand the text back with the extraction rubric ───────────
879
878
  // Deciding what counts as a fact is judgement about *this user's*
@@ -930,7 +929,7 @@ async function handleMemoryTool(name, args) {
930
929
  case "memory_consolidate": {
931
930
  const parsed = ConsolidateSchema.safeParse(args);
932
931
  if (!parsed.success)
933
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
932
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
934
933
  const { topic, limit = 12, summary } = parsed.data;
935
934
  const localConsolidateCfg = (0, local_memory_js_1.getLocalMemoryConfig)();
936
935
  // ── PASS 2: save the caller's merged version ────────────────────────
@@ -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
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MIROSHARK_TOOLS = void 0;
4
4
  exports.handleMirosharkTool = handleMirosharkTool;
5
+ const config_js_1 = require("../config.js");
5
6
  const CONVEX_SITE = process.env.FINCH_CONVEX_URL ?? "https://befitting-porcupine-276.convex.site";
6
7
  exports.MIROSHARK_TOOLS = [
7
8
  {
@@ -50,7 +51,13 @@ exports.MIROSHARK_TOOLS = [
50
51
  ];
51
52
  // ── HTTP helpers ──────────────────────────────────────────────────────────────
52
53
  function authHeaders() {
53
- const key = process.env.FINCH_API_KEY ?? process.env.FINCH_SESSION_TOKEN;
54
+ // Bug fix: this used to read only process.env.FINCH_API_KEY/
55
+ // FINCH_SESSION_TOKEN directly, bypassing getSavedToken()'s env-var-then-
56
+ // saved-config fallback that every other tool goes through (via
57
+ // callConvex/callConvexRaw in convex.ts). A user authenticated via
58
+ // `finch login` (writes to ~/.finch/config.json, not an env var) got a
59
+ // silent, unauthenticated request here while every other tool worked.
60
+ const key = process.env.FINCH_API_KEY ?? (0, config_js_1.getSavedToken)();
54
61
  return key ? { Authorization: `Bearer ${key}` } : {};
55
62
  }
56
63
  async function miroJson(path, method, body, timeoutMs = 90000) {
@@ -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,10 +103,10 @@ 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
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
109
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
110
110
  const key = getKey();
111
111
  if (!key)
112
112
  return noKeyMsg();
@@ -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
  };
@@ -284,7 +284,7 @@ async function handleMonitorTool(name, args) {
284
284
  if (name === "cancel_monitor") {
285
285
  const parsed = CancelSchema.safeParse(args);
286
286
  if (!parsed.success)
287
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
287
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
288
288
  if (args?.confirm !== true) {
289
289
  return {
290
290
  content: [{
package/dist/tools/os.js CHANGED
@@ -6,6 +6,7 @@ const convex_js_1 = require("../convex.js");
6
6
  const token_gate_js_1 = require("../token-gate.js");
7
7
  const wallet_js_1 = require("../wallet.js");
8
8
  const local_memory_js_1 = require("../local-memory.js");
9
+ const config_js_1 = require("../config.js");
9
10
  const CONVEX_SITE = process.env.FINCH_CONVEX_URL ?? "https://befitting-porcupine-276.convex.site";
10
11
  exports.OS_TOOLS = [
11
12
  {
@@ -138,7 +139,6 @@ async function handleOsTool(name, args) {
138
139
  "FIRECRAWL_API_KEY": !!process.env.FIRECRAWL_API_KEY,
139
140
  "FINCH_SESSION_TOKEN": !!process.env.FINCH_SESSION_TOKEN,
140
141
  "FINCH_API_KEY": !!process.env.FINCH_API_KEY,
141
- "TELEGRAM_BOT_TOKEN": !!process.env.TELEGRAM_BOT_TOKEN,
142
142
  };
143
143
  const localMemCfg = (0, local_memory_js_1.getLocalMemoryConfig)();
144
144
  const pingLocalMemory = async () => {
@@ -200,13 +200,18 @@ async function handleOsTool(name, args) {
200
200
  if (!message)
201
201
  return { content: [{ type: "text", text: "Error: message is required" }] };
202
202
  try {
203
- const res = await fetch(`${CONVEX_SITE}/noel/shell/chat`, {
203
+ // Bug fix: this used to read only process.env.FINCH_SESSION_TOKEN/
204
+ // FINCH_API_KEY directly, bypassing getSavedToken()'s fallback to
205
+ // ~/.finch/config.json - a user authenticated via `finch login`
206
+ // (not an env var) silently sent an empty Authorization header here
207
+ // while every other tool (which goes through callConvex) worked.
208
+ const res = await fetch(`${CONVEX_SITE}/finch/shell/chat`, {
204
209
  method: "POST",
205
210
  headers: {
206
211
  "Content-Type": "application/json",
207
- "Authorization": `Bearer ${process.env.FINCH_SESSION_TOKEN ?? process.env.FINCH_API_KEY ?? ""}`,
212
+ "Authorization": `Bearer ${(0, config_js_1.getSavedToken)() ?? process.env.FINCH_API_KEY ?? ""}`,
208
213
  },
209
- body: JSON.stringify({ message, agentId: agent_id ?? "noel-default" }),
214
+ body: JSON.stringify({ message, agentId: agent_id ?? "finch-default" }),
210
215
  signal: AbortSignal.timeout(60000),
211
216
  });
212
217
  if (!res.ok) {
@@ -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: [{
@@ -147,7 +147,7 @@ async function handleResearchChain(name, args) {
147
147
  return null;
148
148
  const parsed = InputSchema.safeParse(args);
149
149
  if (!parsed.success) {
150
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
150
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
151
151
  }
152
152
  const { startKey } = parsed.data;
153
153
  const maxDepth = parsed.data.maxDepth ?? 8;
@@ -154,7 +154,7 @@ async function handleResearchCompare(name, args) {
154
154
  return null;
155
155
  const parsed = InputSchema.safeParse(args);
156
156
  if (!parsed.success) {
157
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
157
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
158
158
  }
159
159
  const { keyA, keyB, focus, comparison } = parsed.data;
160
160
  const saveToVault = parsed.data.saveToVault ?? true;
@@ -100,7 +100,7 @@ async function handleResearchTool(name, args) {
100
100
  if (name === "web_scrape") {
101
101
  const parsed = ScrapeSchema.safeParse(args);
102
102
  if (!parsed.success)
103
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
103
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
104
104
  const { url, focus } = parsed.data;
105
105
  const unsafe = await (0, public_url_js_1.assertPublicUrl)(url);
106
106
  if (unsafe) {
@@ -120,7 +120,7 @@ async function handleResearchTool(name, args) {
120
120
  if (name === "web_search") {
121
121
  const parsed = SearchSchema.safeParse(args);
122
122
  if (!parsed.success)
123
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
123
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
124
124
  const { query, limit = 5 } = parsed.data;
125
125
  // Priority 1: user BYOK direct path. Priority 2: Finch proxy.
126
126
  // Same data shape so the formatting code below is identical for both.
@@ -94,7 +94,7 @@ async function handleBridgeTool(name, args) {
94
94
  return null;
95
95
  const parsed = Schema.safeParse(args);
96
96
  if (!parsed.success) {
97
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
97
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
98
98
  }
99
99
  const wanted = parsed.data.ticker?.trim().toUpperCase();
100
100
  const minGap = parsed.data.minGapPct ?? 0;
@@ -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;
@@ -29,6 +30,20 @@ exports.RH_RPC = process.env.ROBINHOOD_RPC_URL ??
29
30
  process.env.RH_RPC_URL ??
30
31
  process.env.FINCH_RH_RPC_URL ??
31
32
  "https://rpc.mainnet.chain.robinhood.com";
33
+ /** Host only, for display in tool output - never the full RH_RPC. An
34
+ * operator can point ROBINHOOD_RPC_URL/RH_RPC_URL at a keyed provider
35
+ * (same pattern app/convex just fixed for its own Alchemy-key leak risk -
36
+ * a `.../v2/<key>`-style URL echoed verbatim into a tool response lands the
37
+ * key in chat transcripts/client logs). Falls back to the raw value only
38
+ * if it isn't a parseable URL, which should never happen in practice. */
39
+ function rhRpcDisplay() {
40
+ try {
41
+ return new URL(exports.RH_RPC).host;
42
+ }
43
+ catch {
44
+ return "(configured)";
45
+ }
46
+ }
32
47
  exports.RH_EXPLORER = "https://robinhoodchain.blockscout.com";
33
48
  const NATIVE_ETH = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
34
49
  const RH_PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
@@ -753,7 +768,7 @@ async function fetchRhBalances(address) {
753
768
  else
754
769
  for (const h of holdings)
755
770
  lines.push(`- ${h.symbol}: ${h.bal} \`${h.address}\``);
756
- lines.push(``, `_RPC: ${exports.RH_RPC}_`, `_Explorer: ${exports.RH_EXPLORER}/address/${address}_`);
771
+ lines.push(``, `_RPC: ${rhRpcDisplay()}_`, `_Explorer: ${exports.RH_EXPLORER}/address/${address}_`);
757
772
  return lines.join("\n");
758
773
  }
759
774
  // ─── Onchain safety scan (Blockscout + DexScreener; free, no LLM) ─────────────
@@ -990,7 +1005,16 @@ async function quoteRh(args) {
990
1005
  throw new Error("RH swaps route ETH ↔ token. Buy a token with ETH, or sell a token for ETH.");
991
1006
  }
992
1007
  const sellAmount = parseHumanToWei(args.amount, from.decimals);
993
- const slippageBps = Math.round((args.maxSlippagePct ?? 2.0) * 100);
1008
+ const slippagePct = args.maxSlippagePct ?? 2.0;
1009
+ // Same bound defi.ts's SwapSchema already enforces for Base swaps
1010
+ // (.positive().max(50)) - this file had no equivalent check anywhere, so a
1011
+ // negative or absurd value flowed straight through to the backend as
1012
+ // slippageBps, both for direct rh_mcp_swap calls and for every unattended
1013
+ // rh_orders_tick execution of a DCA/bracket order created with a bad value.
1014
+ if (!(slippagePct > 0) || slippagePct > 50) {
1015
+ throw new Error(`maxSlippagePct must be greater than 0 and at most 50 (got ${args.maxSlippagePct}).`);
1016
+ }
1017
+ const slippageBps = Math.round(slippagePct * 100);
994
1018
  const result = await (0, convex_js_1.callConvex)("/mcp/rh/quote", "POST", {
995
1019
  sellToken: from.address,
996
1020
  buyToken: to.address,
@@ -999,7 +1023,7 @@ async function quoteRh(args) {
999
1023
  slippageBps,
1000
1024
  fromSymbol: from.symbol,
1001
1025
  toSymbol: to.symbol,
1002
- }, "rh_mcp_estimate");
1026
+ }, args.toolName ?? "rh_mcp_estimate");
1003
1027
  if (result.error)
1004
1028
  throw new Error(result.error);
1005
1029
  return { ...result, from, to, sellAmount, slippageBps };
@@ -1167,7 +1191,7 @@ async function handleRhMcpTool(name, args) {
1167
1191
  "",
1168
1192
  `**Wallet**: \`${addr ?? "(not configured — run finch login / first tool)"}\``,
1169
1193
  `**ETH on RH (gas)**: ${eth}`,
1170
- `**RPC**: ${exports.RH_RPC}`,
1194
+ `**RPC**: ${rhRpcDisplay()}`,
1171
1195
  `**Explorer**: ${exports.RH_EXPLORER}`,
1172
1196
  `**Catalog**: ${exports.RH_STOCKS.length} stocks (AAPL…USAR) + any RH-chain crypto via DexScreener`,
1173
1197
  "",
@@ -1269,6 +1293,7 @@ async function handleRhMcpTool(name, args) {
1269
1293
  amount: String(a.amount),
1270
1294
  maxSlippagePct: a.maxSlippagePct,
1271
1295
  taker: wallet.address,
1296
+ toolName: "rh_mcp_swap",
1272
1297
  });
1273
1298
  const quote = q.quote ?? q;
1274
1299
  const tx = quote.transaction ?? {