@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
package/README.md CHANGED
@@ -1,12 +1,20 @@
1
1
  <div align="center">
2
2
 
3
- # Finch
3
+ <img src="assets/banner.svg" alt="Finch MCP" width="100%"/>
4
+
5
+ # Finch MCP
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@finchagentic/mcp?color=000000&label=npm&logo=npm)](https://www.npmjs.com/package/@finchagentic/mcp)
8
+ [![Official MCP Registry](https://img.shields.io/badge/MCP_Registry-io.github.finchagentic%2Fmcp-000000)](https://registry.modelcontextprotocol.io)
9
+ [![License: MIT](https://img.shields.io/badge/license-MIT-000000.svg)](LICENSE)
10
+ [![Node](https://img.shields.io/badge/node-%E2%89%A518-000000?logo=node.js&logoColor=white)](package.json)
11
+ [![Tools: 116](https://img.shields.io/badge/tools-116-000000)](#what-you-get)
4
12
 
5
13
  **The runtime layer for Agentic AI.**
6
14
 
7
15
  Persistent memory, autonomous agents, and workflows that survive every session.
8
16
 
9
- [Docs](https://docs.finch.com) · [App](https://app.finchagentic.com) · [npm](https://www.npmjs.com/package/@finchagentic/mcp) · [GitHub](https://github.com/finchagentic/mcp) · [X](https://x.com/finchagentic)
17
+ [App](https://app.finchagentic.com) · [Docs](https://docs.finch.com) · [npm](https://www.npmjs.com/package/@finchagentic/mcp) · [X](https://x.com/finchagentic)
10
18
 
11
19
  </div>
12
20
 
@@ -29,13 +37,13 @@ Always pin the version. Never use `@latest`.
29
37
 
30
38
  ```bash
31
39
  # One-command installer (detects common MCP clients)
32
- npx -y -p @finchagentic/mcp@4.6.1 finch install
40
+ npx -y -p @finchagentic/mcp@4.6.2 finch install
33
41
  ```
34
42
 
35
43
  ### Claude Code
36
44
 
37
45
  ```bash
38
- claude mcp add finch -s user -- npx -y -p @finchagentic/mcp@4.6.1 finch-mcp
46
+ claude mcp add finch -s user -- npx -y -p @finchagentic/mcp@4.6.2 finch-mcp
39
47
  ```
40
48
 
41
49
  ### Cursor / Windsurf / Claude Desktop
@@ -45,24 +53,16 @@ claude mcp add finch -s user -- npx -y -p @finchagentic/mcp@4.6.1 finch-mcp
45
53
  "mcpServers": {
46
54
  "finch": {
47
55
  "command": "npx",
48
- "args": ["-y", "-p", "@finchagentic/mcp@4.6.1", "finch-mcp"]
56
+ "args": ["-y", "-p", "@finchagentic/mcp@4.6.2", "finch-mcp"]
49
57
  }
50
58
  }
51
59
  }
52
60
  ```
53
61
 
54
- ### VS Code
62
+ ### Hermes
55
63
 
56
- ```json
57
- {
58
- "servers": {
59
- "finch": {
60
- "type": "stdio",
61
- "command": "npx",
62
- "args": ["-y", "-p", "@finchagentic/mcp@4.6.1", "finch-mcp"]
63
- }
64
- }
65
- }
64
+ ```bash
65
+ hermes mcp add finch -- npx -y -p @finchagentic/mcp@4.6.2 finch-mcp
66
66
  ```
67
67
 
68
68
  <details>
@@ -79,7 +79,7 @@ claude mcp add finch -s user -- npx -y -p @finchagentic/mcp@4.6.1 finch-mcp
79
79
 
80
80
  </details>
81
81
 
82
- No LLM API key is required to start — 111 of 116 tools are plain reads/writes/on-chain calls that your MCP client's own model already drives; only 5 (`ask_finch`, `deep_research`, and scheduled agent learning) do their own multi-step reasoning server-side and need a key (see [Configuration](#configuration)). Tools load on first use.
82
+ No LLM API key required to start — 111 of 116 tools are plain reads/writes/on-chain calls your client's model already drives. Tools load on first use.
83
83
 
84
84
  ## Quick start
85
85
 
@@ -105,13 +105,11 @@ save this thesis to vault
105
105
  | Pillar | What it does |
106
106
  |--------|----------------|
107
107
  | **Memory** | Full-text searchable memory + versioned vault + chronicle |
108
- | **Agents** | Spawn, recall, update named agents — `agent_recall` also pulls related memory/vault context matching the agent's goal, not just its own logged updates |
108
+ | **Agents** | Spawn, recall, update named agents — `agent_recall` also pulls related memory/vault context matching the agent's goal |
109
109
  | **Workflows** | Automations, monitors, packets, deep research (auto-saves reports + auto-links related past research) |
110
110
  | **Execution** | Base DeFi, Robinhood Chain, market data, web, GitHub |
111
111
 
112
- Coding and research sessions persist the same way: `deep_research` auto-saves its report to vault and links it to related past reports; `code_session_save` does the same for coding/debugging sessions (`vault_save type=code`, versioned per project, auto-linked). Both exist so the next session — yours or another agent's — starts with real context instead of cold.
113
-
114
- `vault_save` and `agent_spawn` also take an optional `workspaceProject` - the same named Projects a user organizes their Agents/vault content into on the webapp's Agents page. Pass a name and it's matched case-insensitively or created automatically (`list_projects` to browse what exists first). Hosted vault only - local-vault mode has no project concept.
112
+ `deep_research` auto-saves its report to vault and links related past reports; `code_session_save` does the same for coding sessions. `vault_save` and `agent_spawn` accept an optional `workspaceProject` matching the webapp's Projects.
115
113
 
116
114
  Default palette is `core` (lighter context). Full set:
117
115
 
@@ -124,8 +122,7 @@ Default palette is `core` (lighter context). Full set:
124
122
  Finch is the runtime. **Your LLM is the brain. Your data stays yours.**
125
123
 
126
124
  ```bash
127
- npx -y -p @finchagentic/mcp@4.6.1 finch setup
128
- # enable local vault (and optional local memory)
125
+ npx -y -p @finchagentic/mcp@4.6.2 finch setup
129
126
  ```
130
127
 
131
128
  | Piece | Location |
@@ -133,65 +130,33 @@ npx -y -p @finchagentic/mcp@4.6.1 finch setup
133
130
  | Vault | `~/.finch/vault/` |
134
131
  | Wallet | `~/.finch/wallet.json` |
135
132
  | Config | `~/.finch/config.json` |
136
- | Brain | your MCP clients model |
133
+ | Brain | your MCP client's model |
137
134
 
138
- Scheduled/cloud features still need an account. Core memory, vault, and public-data tools work offline of Finch cloud.
135
+ ## Registry
139
136
 
140
- ## Configuration
137
+ Finch is listed in the [official MCP Registry](https://registry.modelcontextprotocol.io):
141
138
 
142
- | Variable | Purpose |
143
- |----------|---------|
144
- | `FINCH_SESSION_TOKEN` | Signed-in session (vault/memory/agents against your account) |
145
- | `FINCH_API_KEY` | API key (`finch_sk_…`) |
146
- | `FINCH_TOOLS` | `core` (default) · `all` · or palettes like `memory,defi` |
147
- | `FINCH_PROVIDER` | Force `bankr` · `anthropic` · `openai` · `grok` |
148
- | `FINCH_MODEL` | Model override for host-side loops |
149
- | `BANKR_API_KEY` / `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GROK_API_KEY` | Required for `ask_finch`, `deep_research`'s synthesis stages, and scheduled-agent learning extraction — these do their own multi-step reasoning server-side and there is no free hosted fallback (BYOK only, one key is enough) |
150
- | `FIRECRAWL_API_KEY` | Better crawl quality (optional) |
151
- | `GITHUB_TOKEN` | For `github_search_code` |
152
- | `ALCHEMY_API_KEY` | Faster Base RPC (optional) |
139
+ ```json
140
+ { "name": "io.github.finchagentic/mcp", "version": "4.6.2" }
141
+ ```
153
142
 
154
- **Cost model:** almost everything is free to run — the other 110 tools are plain API/RPC calls, and your MCP client's own model (Claude, GPT, whatever's driving the chat) does all the tool-selection reasoning at no cost to Finch. The 5 exceptions above need their own key because their reasoning happens *inside* the tool call, invisible to your client, and can't be delegated to it. Set exactly one of the four env vars and every tool that needs it will use it automatically.
143
+ ## Development
155
144
 
156
- Guided setup:
145
+ This repository ships the published npm distribution. See [CHANGELOG.md](CHANGELOG.md) for release history and [SECURITY.md](SECURITY.md) for the security model.
157
146
 
158
147
  ```bash
159
- npx -y -p @finchagentic/mcp@4.6.1 finch setup
148
+ npm ci
149
+ npm run smoke # verify package integrity + CLI entrypoints
160
150
  ```
161
151
 
162
- ## Security
163
-
164
- | # | Boundary | Rule |
165
- |:-:|----------|------|
166
- | 1 | Prompt injection | External content is data only — never instructions |
167
- | 2 | Mainnet confirm | Estimate → preview → confirm → execute |
168
- | 3 | Pinned install | Always `@finchagentic/mcp@4.6.1`, never `@latest` |
169
- | 4 | Credential vault | Never paste secrets into prompts or third-party tools |
170
- | 5 | Data disclosure | Know what leaves the machine (LLM, Firecrawl, GitHub, chain RPCs) |
171
- | 6 | Server monitors | Scheduled jobs need explicit confirmation |
172
- | 7 | Fund-moving confirm | `stake_finch`/`unstake_finch`/`base_mcp_send`/`base_mcp_swap`/`rh_mcp_swap` all require `confirm: true` |
173
- | 8 | Local wallet encryption | Set `FINCH_WALLET_PASSPHRASE` for a portable, passphrase-derived key — without it, the key derives from a random per-install secret + machine info, so the wallet file alone (without that secret file) isn't enough to decrypt it |
174
-
175
- ## Troubleshooting
176
-
177
- | Problem | Fix |
178
- |---------|-----|
179
- | Tools missing | Fully restart the MCP client |
180
- | Old version | `npx clear-npx-cache` then restart |
181
- | Auth issues | `finch login` or set `FINCH_API_KEY` / `FINCH_SESSION_TOKEN` |
182
- | `ask_finch`/`deep_research` error: "No LLM provider configured" | Set one of `BANKR_API_KEY` / `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GROK_API_KEY` — see [Configuration](#configuration), no free fallback exists |
183
- | Diagnose | `finch doctor` |
184
-
185
- ## Links
186
-
187
- | | |
188
- |--|--|
189
- | **Docs** | [docs.finch.com](https://docs.finch.com) |
190
- | **App** | [app.finchagentic.com](https://app.finchagentic.com) |
191
- | **npm** | [@finchagentic/mcp](https://www.npmjs.com/package/@finchagentic/mcp) |
192
- | **GitHub** | [github.com/finchagentic/mcp](https://github.com/finchagentic/mcp) |
193
- | **X** | [@finchagentic](https://x.com/finchagentic) |
152
+ ## Support
194
153
 
195
- ---
154
+ - Issues: [github.com/finchagentic/mcp/issues](https://github.com/finchagentic/mcp/issues)
155
+ - Docs: [docs.finch.com](https://docs.finch.com)
156
+ - X: [@finchagentic](https://x.com/finchagentic)
196
157
 
197
- MIT License · Finch
158
+ <div align="center">
159
+
160
+ **Finch** — memory that survives every session.
161
+
162
+ </div>
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ // Shared cache + 429-backoff wrapper for external HTTP calls.
3
+ // Designed for read-heavy public APIs like CoinGecko (free tier: 30 req/min)
4
+ // and DexScreener - agent loops + parallel tool calls were tripping rate
5
+ // limits in production. Cache hit returns the prior body without a network
6
+ // round-trip; cache miss does a fetch with bounded retries on 429/503.
7
+ //
8
+ // The cache is in-process only - every MCP server process keeps its own
9
+ // LRU. That's intentional: tokens get fresh data on cold start, no shared
10
+ // state to invalidate across users.
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.cachedFetch = cachedFetch;
13
+ exports.clearHttpCache = clearHttpCache;
14
+ exports.httpCacheStats = httpCacheStats;
15
+ const DEFAULT_TTL_MS = 45000; // 45s - fresh enough for prices, generous enough to absorb a flurry
16
+ const DEFAULT_MAX_ENTRIES = 200;
17
+ const DEFAULT_RETRY_DELAYS_MS = [500, 1500, 4000];
18
+ const cache = new Map();
19
+ function cacheKey(url, init) {
20
+ // POST bodies are part of the key so two POSTs with different params don't collide.
21
+ if (!init || !init.body || init.method === "GET")
22
+ return `GET ${url}`;
23
+ return `${init.method ?? "POST"} ${url} ${typeof init.body === "string" ? init.body : ""}`;
24
+ }
25
+ function evictExpired() {
26
+ const now = Date.now();
27
+ for (const [k, v] of cache) {
28
+ if (v.expiresAt < now)
29
+ cache.delete(k);
30
+ }
31
+ // LRU-ish eviction - Map preserves insertion order, so the first keys are oldest.
32
+ while (cache.size > DEFAULT_MAX_ENTRIES) {
33
+ const firstKey = cache.keys().next().value;
34
+ if (firstKey === undefined)
35
+ break;
36
+ cache.delete(firstKey);
37
+ }
38
+ }
39
+ async function cachedFetch(url, init = {}, opts = {}) {
40
+ const ttl = opts.ttlMs ?? DEFAULT_TTL_MS;
41
+ const retryDelays = opts.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS;
42
+ const timeout = opts.timeoutMs ?? 15000;
43
+ evictExpired();
44
+ const key = cacheKey(url, init);
45
+ if (!opts.bypassCache) {
46
+ const hit = cache.get(key);
47
+ if (hit && hit.expiresAt > Date.now()) {
48
+ // Refresh recency - re-insert moves it to the end of the Map.
49
+ cache.delete(key);
50
+ cache.set(key, hit);
51
+ return { ok: hit.status >= 200 && hit.status < 300, status: hit.status, text: hit.body, fromCache: true };
52
+ }
53
+ }
54
+ let lastStatus = 0;
55
+ let lastText = "";
56
+ for (let attempt = 0; attempt <= retryDelays.length; attempt++) {
57
+ try {
58
+ const res = await fetch(url, { ...init, signal: AbortSignal.timeout(timeout) });
59
+ const text = await res.text();
60
+ lastStatus = res.status;
61
+ lastText = text;
62
+ // Cache successful responses + 404s (404 is "definitively not found" - no point retrying).
63
+ if (res.ok || res.status === 404) {
64
+ cache.set(key, { body: text, status: res.status, expiresAt: Date.now() + ttl });
65
+ return { ok: res.ok, status: res.status, text, fromCache: false };
66
+ }
67
+ // Retry on 429 (rate limit) and 5xx (transient server errors).
68
+ if (res.status === 429 || res.status >= 500) {
69
+ const delay = retryDelays[attempt];
70
+ if (delay === undefined)
71
+ break;
72
+ // Honor server's Retry-After if provided (CoinGecko sends this).
73
+ const retryAfter = res.headers.get("retry-after");
74
+ const waitMs = retryAfter ? Math.min(parseInt(retryAfter) * 1000, 30000) : delay;
75
+ await new Promise((r) => setTimeout(r, waitMs));
76
+ continue;
77
+ }
78
+ // Non-retryable client error (400/401/403) - surface to caller without retry.
79
+ return { ok: false, status: res.status, text, fromCache: false };
80
+ }
81
+ catch (err) {
82
+ lastText = err instanceof Error ? err.message : String(err);
83
+ const delay = retryDelays[attempt];
84
+ if (delay === undefined)
85
+ break;
86
+ await new Promise((r) => setTimeout(r, delay));
87
+ }
88
+ }
89
+ return { ok: false, status: lastStatus, text: lastText, fromCache: false };
90
+ }
91
+ function clearHttpCache() {
92
+ cache.clear();
93
+ }
94
+ function httpCacheStats() {
95
+ return { size: cache.size, maxEntries: DEFAULT_MAX_ENTRIES };
96
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ // Shared substring/keyword scoring for the local-file backends
3
+ // (local-memory-file.ts, local-vault.ts) - no embeddings, no server, zero
4
+ // dependencies. Word-boundary aware, not raw substring counting - a short
5
+ // common term like "is"/"a" used to score a "match" purely by being a
6
+ // substring of an unrelated word ("is" inside "distances", "a" inside
7
+ // "banana"). Found via real testing of memory_add's new conflict-hint
8
+ // feature: a query for "the user's favorite pizza topping is pepperoni"
9
+ // registered as "related" to a completely unrelated stored memory about
10
+ // metric vs imperial units, purely because both contained "the" and "is".
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.meaningfulTerms = meaningfulTerms;
13
+ exports.wordOccurrences = wordOccurrences;
14
+ const STOP_WORDS = new Set([
15
+ "a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
16
+ "to", "of", "in", "on", "at", "for", "with", "by", "from", "as",
17
+ "and", "or", "but", "if", "so", "this", "that", "it", "its", "i",
18
+ "you", "your", "they", "them", "their", "he", "she", "his", "her",
19
+ "not", "no", "do", "does", "did", "has", "have", "had", "will", "would",
20
+ ]);
21
+ /** Query terms worth scoring against - lowercased, stop-words and
22
+ * single-character noise dropped. An empty result means the query was
23
+ * entirely stop words/punctuation - callers should treat that as "no
24
+ * meaningful query" (return no matches) rather than matching everything. */
25
+ function meaningfulTerms(query) {
26
+ return query
27
+ .toLowerCase()
28
+ .split(/[^a-z0-9]+/)
29
+ .filter((t) => t.length > 1 && !STOP_WORDS.has(t));
30
+ }
31
+ function escapeRegex(s) {
32
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
33
+ }
34
+ /** Word-boundary occurrence count of `term` inside `haystack` - not a raw
35
+ * substring count, so "is" doesn't match inside "distances" or "this". */
36
+ function wordOccurrences(haystack, term) {
37
+ const re = new RegExp(`\\b${escapeRegex(term)}\\b`, "g");
38
+ return (haystack.match(re) || []).length;
39
+ }
@@ -0,0 +1,301 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runAgent = runAgent;
4
+ const server_js_1 = require("./server.js");
5
+ const llm_js_1 = require("./llm.js");
6
+ const convex_js_1 = require("./convex.js");
7
+ const SYSTEM_PROMPT = [
8
+ `You are Finch, the runtime layer for Agentic AI - with ${server_js_1.ALL_TOOLS.length} tools spanning memory, vault, deep research, persistent agents, code, automations, DeFi, and GitHub.`,
9
+ "Be direct and concise. Pick the right tool - don't narrate the choice. Summarize tool results in plain English.",
10
+ "",
11
+ "CRITICAL - on-chain / financial actions (swap, send, transfer, buy, sell, bridge, lend, deposit, withdraw, balance):",
12
+ "- You MUST call the corresponding tool. NEVER write 'Sent', 'Swapped', 'Tx confirmed', or any execution claim without an actual tool call returning a result first.",
13
+ "- Tx hash and basescan/explorer URL from the tool output are MANDATORY in your reply - show them verbatim, do not omit or paraphrase.",
14
+ "- NEVER fabricate a post-transaction balance with arithmetic. If user wants the new balance, call the balance tool again - do not compute it from a prior balance + amount.",
15
+ "- For ALL Base chain operations, you MUST use the base_mcp_* family: base_mcp_swap (NOT swap_tokens), base_mcp_send (NOT send_token), base_mcp_balance (NOT get_portfolio), base_mcp_estimate, base_mcp_resolve, base_mcp_lend, base_mcp_status. This is non-negotiable - Base operations go through the Base MCP skill, period.",
16
+ "- For Robinhood Chain tokenized stocks (chainId 4663, NVDA/AAPL/etc on Uniswap V4), you MUST use rh_mcp_*: rh_mcp_status, rh_mcp_list_stocks, rh_mcp_balance, rh_mcp_estimate, rh_mcp_swap. Never use base_mcp_* or 0x for RH stocks. rh_mcp_swap requires confirm:true. Explorer = robinhoodchain.blockscout.com. This is NOT Robinhood Agentic brokerage (agent.robinhood.com).",
17
+ "- Do NOT claim an address belongs to the user (e.g. 'your own address') unless you have verified ownership. Resolving a basename returns whoever owns that name - usually NOT the caller.",
18
+ "- The Finch wallet is the SAME address on both Base and Robinhood Chain, but they are separate ledgers - a balance on one tells you nothing about the other. If the user asks a chain-unspecified question ('what's my balance', 'do I have any funds') call BOTH base_mcp_balance AND rh_mcp_balance before answering. Never report only one chain's result as 'your balance' or 'your wallet is empty' - say which chain(s) you checked, and give both figures.",
19
+ "- Swap/send requests are only chain-unambiguous when the token itself pins the chain (a RH catalog stock like NVDA/AAPL, or a token you already know only exists on one side). A bare request like 'swap 0.001 ETH to a stablecoin' with no chain named is NOT unambiguous just because you picked one - ETH and 'stablecoin' both exist on Base (USDC/USDT/DAI) and Robinhood Chain (USDG). Before estimating or executing an ambiguous swap, call both base_mcp_balance and rh_mcp_balance first: if only one chain actually holds enough of the source token, use that chain and say which one and why; if neither holds enough, say so plainly instead of quoting a swap the wallet can't cover; if both hold enough, ask the user which chain before proceeding.",
20
+ "- There is NO send/transfer tool for Robinhood Chain - base_mcp_send only moves assets on Base mainnet. If the user asks to send/transfer USDG or any RH catalog stock (NVDA, AAPL, etc.), do NOT call base_mcp_send with that token name hoping it resolves - it will either error or, worse, silently match an unrelated Base token with the same symbol. Tell the user directly that on-chain sends are not yet supported on Robinhood Chain.",
21
+ "- Never state a balance, price, quote, token address, or transaction result from memory or inference - every number in your answer must trace to a tool call you made THIS turn. If you are not sure which chain, token, or address a term refers to, ask or resolve it (rh_token_resolve / base_mcp_resolve) before answering - do not guess and present the guess as fact.",
22
+ "",
23
+ "For deep research: prefer deep_research (multi-stage, saves to vault). Use continueFrom when extending prior reports.",
24
+ "For live web info: use web_search. For market questions: use get_market_data or market_thesis.",
25
+ "Save substantive findings to vault; do not save thin or empty outputs.",
26
+ "",
27
+ "NEVER call ask_finch from this shell. It exists for MCP clients that have no reasoning model of their own - you already are one. Calling it mid-task means asking a second model to think for you, which produces nothing you couldn't write yourself from the data you already have, and repeating it when unsatisfied just burns turns. If a tool's result already answers the question, write the answer yourself.",
28
+ "Do not call the same tool with the same or near-identical arguments more than once in a single answer. If deep_research, a search, or an analysis tool already returned data, synthesize from that - do not re-run it hoping for a different result, and do not chain more tool calls than the question actually needs. Stop calling tools and answer as soon as you have enough to answer well.",
29
+ ].join("\n");
30
+ async function runAgent(userMessage, history, onToolCall) {
31
+ const provider = process.env.FINCH_PROVIDER?.toLowerCase().trim();
32
+ const bankrKey = process.env.BANKR_API_KEY;
33
+ const anthropicKey = process.env.ANTHROPIC_API_KEY;
34
+ const openaiKey = process.env.OPENAI_API_KEY;
35
+ const grokKey = process.env.GROK_API_KEY;
36
+ // Explicit override - lets FINCH_PROVIDER=openai win even when BANKR_API_KEY
37
+ // is also set (e.g. as a persistent shell env var), same as llm.ts's callLLM.
38
+ if (provider === "bankr" && bankrKey)
39
+ return runBankrLoop(bankrKey, userMessage, history, onToolCall);
40
+ if (provider === "anthropic" && anthropicKey)
41
+ return runAnthropicLoop(anthropicKey, userMessage, history, onToolCall);
42
+ if (provider === "openai" && openaiKey)
43
+ return runOpenAILoop(openaiKey, userMessage, history, onToolCall);
44
+ if (provider === "grok" && grokKey)
45
+ return runGrokLoop(grokKey, userMessage, history, onToolCall);
46
+ // Auto-priority - matches llm.ts's callLLM() order exactly (bankr →
47
+ // anthropic → openai → grok → Convex proxy) so a one-shot LLM tool call
48
+ // (ask_finch, memory extraction, etc.) and the interactive agent loop
49
+ // never silently pick different providers for the same configured keys.
50
+ // A GROK_API_KEY-only user used to fall all the way through to the
51
+ // Convex-proxied Anthropic loop here despite callLLM() using Grok.
52
+ if (bankrKey)
53
+ return runBankrLoop(bankrKey, userMessage, history, onToolCall);
54
+ if (anthropicKey)
55
+ return runAnthropicLoop(anthropicKey, userMessage, history, onToolCall);
56
+ if (openaiKey)
57
+ return runOpenAILoop(openaiKey, userMessage, history, onToolCall);
58
+ if (grokKey)
59
+ return runGrokLoop(grokKey, userMessage, history, onToolCall);
60
+ // No direct key - proxy through Finch backend. Wallet auto-creates at ~/.finch/wallet.json
61
+ // on first use and signs requests transparently. No account or config needed.
62
+ try {
63
+ return await runConvexProxiedLoop(userMessage, history, onToolCall);
64
+ }
65
+ catch {
66
+ // Network down or backend unavailable - plain chat fallback
67
+ const text = await (0, llm_js_1.callLLM)(SYSTEM_PROMPT, userMessage, 1024, history);
68
+ return { text, toolCalls: [] };
69
+ }
70
+ }
71
+ // ── Anthropic agent loop ─────────────────────────────────────────────────────
72
+ function toAnthropicTool(tool) {
73
+ return {
74
+ name: tool.name,
75
+ description: tool.description ?? "",
76
+ input_schema: tool.inputSchema ?? { type: "object", properties: {} },
77
+ };
78
+ }
79
+ // Shared loop body for any provider that speaks Anthropic's native
80
+ // content-block format (tool_use/tool_result) - runAnthropicLoop (direct
81
+ // api.anthropic.com) and runConvexProxiedLoop (same wire format via Finch's
82
+ // /llm/complete proxy) previously duplicated this ~70-line body verbatim,
83
+ // with only the transport (sendTurn/sendFinal) differing. Keeping one copy
84
+ // means a future fix (retry-on-5xx, tool-result truncation, etc.) can't be
85
+ // applied to one provider and silently miss the other.
86
+ async function runAnthropicStyleLoop(sendTurn, sendFinal, userMessage, history, onToolCall) {
87
+ const tools = server_js_1.ALL_TOOLS.map(toAnthropicTool);
88
+ const toolCalls = [];
89
+ const messages = [
90
+ ...history.map(h => ({ role: h.role, content: h.content })),
91
+ { role: "user", content: userMessage },
92
+ ];
93
+ for (let turn = 0; turn < 10; turn++) {
94
+ const data = await sendTurn(messages, tools);
95
+ messages.push({ role: "assistant", content: data.content });
96
+ if (data.stop_reason !== "tool_use") {
97
+ const text = data.content
98
+ .filter(b => b.type === "text")
99
+ .map(b => b.text)
100
+ .join("");
101
+ return { text, toolCalls };
102
+ }
103
+ // Execute all tool_use blocks
104
+ const toolResults = [];
105
+ for (const block of data.content) {
106
+ if (block.type !== "tool_use")
107
+ continue;
108
+ let resultText;
109
+ try {
110
+ const handler = server_js_1.HANDLER_MAP.get(block.name);
111
+ if (!handler)
112
+ throw new Error(`Unknown tool: ${block.name}`);
113
+ // Recorded only once the handler is confirmed to exist - an "Unknown
114
+ // tool" miss is a model error, not a real tool execution, and callers
115
+ // reading AgentResult.toolCalls should be able to trust every entry
116
+ // actually ran.
117
+ onToolCall(block.name);
118
+ toolCalls.push({ name: block.name });
119
+ const result = await handler(block.name, block.input ?? {});
120
+ // A handler returning null (a name declared in its *_TOOLS array with
121
+ // no matching branch - a latent bug ruled out today but not
122
+ // structurally prevented) must not fall through to "Done." - that
123
+ // would report success to the model for a call that never actually
124
+ // ran, directly contradicting this loop's own system-prompt rule
125
+ // against claiming an unverified result.
126
+ if (result === null)
127
+ throw new Error(`Tool ${block.name} returned no result (handler bug - not executed)`);
128
+ resultText = result.content?.[0]?.text ?? "Done.";
129
+ }
130
+ catch (err) {
131
+ resultText = `Error: ${err.message}`;
132
+ }
133
+ toolResults.push({ type: "tool_result", tool_use_id: block.id, content: resultText });
134
+ }
135
+ messages.push({ role: "user", content: toolResults });
136
+ }
137
+ // Hit the turn cap without a final answer - rather than hand back nothing,
138
+ // force one more call with no tools available so the model has to
139
+ // synthesize a real answer from whatever it already gathered.
140
+ return finishWithoutTools(sendFinal, messages, toolCalls);
141
+ }
142
+ function runAnthropicLoop(apiKey, userMessage, history, onToolCall) {
143
+ const model = process.env.FINCH_MODEL ?? process.env.ANTHROPIC_MODEL ?? "claude-haiku-4-5-20251001";
144
+ const sendTurn = async (messages, tools) => {
145
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
146
+ method: "POST",
147
+ headers: {
148
+ "Content-Type": "application/json",
149
+ "x-api-key": apiKey,
150
+ "anthropic-version": "2023-06-01",
151
+ },
152
+ body: JSON.stringify({ model, max_tokens: 4096, system: SYSTEM_PROMPT, tools, messages }),
153
+ signal: AbortSignal.timeout(90000),
154
+ });
155
+ if (!res.ok) {
156
+ const body = await res.text().catch(() => "");
157
+ throw new Error(`Anthropic ${res.status}: ${body.slice(0, 300)}`);
158
+ }
159
+ return await res.json();
160
+ };
161
+ const sendFinal = async (msgs) => {
162
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
163
+ method: "POST",
164
+ headers: { "Content-Type": "application/json", "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
165
+ body: JSON.stringify({ model, max_tokens: 4096, system: SYSTEM_PROMPT, messages: msgs }),
166
+ signal: AbortSignal.timeout(90000),
167
+ });
168
+ if (!res.ok)
169
+ return "";
170
+ const data = await res.json();
171
+ return (data.content ?? []).filter(b => b.type === "text").map(b => b.text).join("");
172
+ };
173
+ return runAnthropicStyleLoop(sendTurn, sendFinal, userMessage, history, onToolCall);
174
+ }
175
+ // Shared turn-cap fallback: rather than "Reached max tool iterations." with
176
+ // nothing useful, ask the model to synthesize a real answer from whatever
177
+ // conversation history (including every tool result so far) it already has,
178
+ // with no tools offered so it can't keep deferring.
179
+ async function finishWithoutTools(call, messages, toolCalls) {
180
+ try {
181
+ const closingMessages = [
182
+ ...messages,
183
+ { role: "user", content: "Stop calling tools. Summarize what you found above into a direct answer for the user right now." },
184
+ ];
185
+ const text = await call(closingMessages);
186
+ if (text.trim())
187
+ return { text, toolCalls };
188
+ }
189
+ catch { /* fall through to the honest failure message below */ }
190
+ return {
191
+ text: "I gathered some information but ran out of turns before finishing the analysis. Try a narrower question, or ask me to continue from what I found.",
192
+ toolCalls,
193
+ };
194
+ }
195
+ // ── Convex-proxied Anthropic loop (session token only - platform covers LLM) ──
196
+ function runConvexProxiedLoop(userMessage, history, onToolCall) {
197
+ const model = process.env.FINCH_MODEL ?? process.env.ANTHROPIC_MODEL ?? "claude-haiku-4-5-20251001";
198
+ // callConvex handles wallet/session auth automatically; 90s timeout matches the proxy endpoint
199
+ const sendTurn = (messages, tools) => (0, convex_js_1.callConvex)("/llm/complete", "POST", { model, max_tokens: 4096, system: SYSTEM_PROMPT, tools, messages }, "llm_complete", 90000);
200
+ const sendFinal = async (msgs) => {
201
+ const data = await (0, convex_js_1.callConvex)("/llm/complete", "POST", {
202
+ model, max_tokens: 4096, system: SYSTEM_PROMPT, messages: msgs,
203
+ }, "llm_complete", 90000);
204
+ return (data.content ?? []).filter((b) => b.type === "text").map((b) => b.text).join("");
205
+ };
206
+ return runAnthropicStyleLoop(sendTurn, sendFinal, userMessage, history, onToolCall);
207
+ }
208
+ // ── Bankr (OpenAI-compatible) agent loop ─────────────────────────────────────
209
+ function toBankrTool(tool) {
210
+ return {
211
+ type: "function",
212
+ function: {
213
+ name: tool.name,
214
+ description: tool.description ?? "",
215
+ parameters: tool.inputSchema ?? { type: "object", properties: {} },
216
+ },
217
+ };
218
+ }
219
+ // Shared tool-calling loop for any OpenAI Chat Completions-compatible
220
+ // endpoint (Bankr's LLM gateway and OpenAI itself both speak this format).
221
+ // Only the URL, auth header, and model differ per provider.
222
+ async function runOpenAICompatibleLoop(url, authHeaders, model, providerLabel, userMessage, history, onToolCall) {
223
+ const tools = server_js_1.ALL_TOOLS.map(toBankrTool);
224
+ const toolCalls = [];
225
+ const messages = [
226
+ { role: "system", content: SYSTEM_PROMPT },
227
+ ...history.map(h => ({ role: h.role, content: h.content })),
228
+ { role: "user", content: userMessage },
229
+ ];
230
+ for (let turn = 0; turn < 10; turn++) {
231
+ const res = await fetch(url, {
232
+ method: "POST",
233
+ headers: { "Content-Type": "application/json", ...authHeaders },
234
+ body: JSON.stringify({ model, messages, tools, max_tokens: 4096 }),
235
+ signal: AbortSignal.timeout(90000),
236
+ });
237
+ if (!res.ok) {
238
+ const body = await res.text().catch(() => "");
239
+ throw new Error(`${providerLabel} ${res.status}: ${body.slice(0, 300)}`);
240
+ }
241
+ const data = await res.json();
242
+ const choice = data.choices?.[0]?.message;
243
+ if (!choice)
244
+ throw new Error(`Empty response from ${providerLabel}`);
245
+ messages.push(choice);
246
+ if (!choice.tool_calls?.length) {
247
+ return { text: choice.content ?? "", toolCalls };
248
+ }
249
+ for (const call of choice.tool_calls) {
250
+ let resultText;
251
+ try {
252
+ const args = JSON.parse(call.function.arguments ?? "{}");
253
+ const handler = server_js_1.HANDLER_MAP.get(call.function.name);
254
+ if (!handler)
255
+ throw new Error(`Unknown tool: ${call.function.name}`);
256
+ // Recorded only once the handler is confirmed to exist - see the
257
+ // matching comment in runAnthropicLoop/runConvexProxiedLoop.
258
+ onToolCall(call.function.name);
259
+ toolCalls.push({ name: call.function.name });
260
+ const result = await handler(call.function.name, args);
261
+ resultText = result?.content?.[0]?.text ?? "Done.";
262
+ }
263
+ catch (err) {
264
+ resultText = `Error: ${err.message}`;
265
+ }
266
+ messages.push({ role: "tool", tool_call_id: call.id, content: resultText });
267
+ }
268
+ }
269
+ return finishWithoutTools(async (msgs) => {
270
+ const res = await fetch(url, {
271
+ method: "POST",
272
+ headers: { "Content-Type": "application/json", ...authHeaders },
273
+ body: JSON.stringify({ model, messages: msgs, max_tokens: 4096 }),
274
+ signal: AbortSignal.timeout(90000),
275
+ });
276
+ if (!res.ok)
277
+ return "";
278
+ const data = await res.json();
279
+ return data.choices?.[0]?.message?.content ?? "";
280
+ }, messages, toolCalls);
281
+ }
282
+ async function runBankrLoop(apiKey, userMessage, history, onToolCall) {
283
+ const model = process.env.FINCH_MODEL ?? process.env.BANKR_MODEL ?? "claude-haiku-4-5-20251001";
284
+ return runOpenAICompatibleLoop("https://llm.bankr.bot/v1/chat/completions", { "X-API-Key": apiKey }, model, "Bankr", userMessage, history, onToolCall);
285
+ }
286
+ // Same OPENAI_BASE_URL override as llm.ts's callOpenAI - lets tool-calling
287
+ // route to a self-hosted OpenAI-compatible gateway too.
288
+ function openAiChatUrl() {
289
+ const base = process.env.OPENAI_BASE_URL?.replace(/\/+$/, "");
290
+ return base ? `${base}/chat/completions` : "https://api.openai.com/v1/chat/completions";
291
+ }
292
+ async function runOpenAILoop(apiKey, userMessage, history, onToolCall) {
293
+ const model = process.env.FINCH_MODEL ?? process.env.OPENAI_MODEL ?? "gpt-4o-mini";
294
+ return runOpenAICompatibleLoop(openAiChatUrl(), { Authorization: `Bearer ${apiKey}` }, model, "OpenAI", userMessage, history, onToolCall);
295
+ }
296
+ // xAI's Chat Completions API is OpenAI-compatible (same as llm.ts's callGrok),
297
+ // so this reuses runOpenAICompatibleLoop rather than a bespoke loop.
298
+ async function runGrokLoop(apiKey, userMessage, history, onToolCall) {
299
+ const model = process.env.FINCH_MODEL ?? process.env.FINCH_GROK_MODEL ?? process.env.GROK_MODEL ?? "grok-4-fast-reasoning";
300
+ return runOpenAICompatibleLoop("https://api.x.ai/v1/chat/completions", { Authorization: `Bearer ${apiKey}` }, model, "Grok", userMessage, history, onToolCall);
301
+ }