@finchagentic/mcp 4.7.1 → 4.7.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 (51) hide show
  1. package/README.md +74 -39
  2. package/dist/_zod-helpers.js +10 -0
  3. package/dist/cli-doctor.js +300 -0
  4. package/dist/cli-env.js +91 -0
  5. package/dist/cli-install.js +236 -0
  6. package/dist/cli-login.js +101 -0
  7. package/dist/cli-orders.js +175 -0
  8. package/dist/cli-setup.js +270 -0
  9. package/dist/cli-ui.js +168 -0
  10. package/dist/cli-vault.js +75 -0
  11. package/dist/cli.js +61 -1152
  12. package/dist/config.js +3 -2
  13. package/dist/enrichment-router.js +5 -18
  14. package/dist/finch-output.js +30 -29
  15. package/dist/finch-status.js +105 -40
  16. package/dist/index.js +0 -0
  17. package/dist/local-vault.js +5 -12
  18. package/dist/output-schemas.js +3 -12
  19. package/dist/project.js +4 -13
  20. package/dist/server.js +13 -35
  21. package/dist/tool-filter.js +4 -13
  22. package/dist/tools/_solidity-scan.js +5 -14
  23. package/dist/tools/agents.js +14 -26
  24. package/dist/tools/deep-research-constants.js +33 -0
  25. package/dist/tools/deep-research-firecrawl.js +88 -0
  26. package/dist/tools/deep-research-planning.js +249 -0
  27. package/dist/tools/deep-research-synthesis.js +343 -0
  28. package/dist/tools/deep-research-text.js +158 -0
  29. package/dist/tools/deep-research-tools.js +90 -0
  30. package/dist/tools/deep-research.js +47 -938
  31. package/dist/tools/defi.js +4 -3
  32. package/dist/tools/insider.js +4 -14
  33. package/dist/tools/insight.js +7 -6
  34. package/dist/tools/market.js +16 -15
  35. package/dist/tools/memory.js +57 -62
  36. package/dist/tools/monitor.js +7 -6
  37. package/dist/tools/os.js +7 -85
  38. package/dist/tools/research.js +7 -6
  39. package/dist/tools/rh-mcp-constants.js +65 -0
  40. package/dist/tools/rh-mcp-dex.js +107 -0
  41. package/dist/tools/rh-mcp-provider.js +127 -0
  42. package/dist/tools/rh-mcp-resolve.js +137 -0
  43. package/dist/tools/rh-mcp-risk.js +230 -0
  44. package/dist/tools/rh-mcp-safety.js +234 -0
  45. package/dist/tools/rh-mcp-swap.js +181 -0
  46. package/dist/tools/rh-mcp-tools.js +137 -0
  47. package/dist/tools/rh-mcp.js +75 -1191
  48. package/dist/tools/scanner.js +10 -9
  49. package/dist/tools/stake.js +7 -20
  50. package/dist/tools/vault.js +52 -59
  51. package/package.json +12 -9
@@ -0,0 +1,234 @@
1
+ "use strict";
2
+ // Onchain safety scan (Blockscout + DexScreener; free, no LLM) — powers
3
+ // rh_safety_check.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.blockscoutSafety = blockscoutSafety;
6
+ exports.assessSafety = assessSafety;
7
+ exports.buildRhSafetyStructured = buildRhSafetyStructured;
8
+ exports.renderSafety = renderSafety;
9
+ const rh_mcp_constants_js_1 = require("./rh-mcp-constants.js");
10
+ // Factory address → launchpad. Keyed lowercase. Only put an entry here once the
11
+ // factory has been read off-chain on 4663 — a wrong mapping silently mislabels
12
+ // every token a factory ever deployed.
13
+ // Observed empirically by sampling recent deployments, not taken from docs.
14
+ // Add an entry only after seeing the factory actually deploy a live token.
15
+ const LAUNCHPAD_FACTORIES = {
16
+ "0xa5aab3f0c6eeadf30ef1d3eb997108e976351feb": {
17
+ label: "Pons",
18
+ note: "PonsLaunchFactory — the dominant launchpad on this chain",
19
+ },
20
+ "0x1b37d3a72082029c44b35b604ea473617580b69a": {
21
+ label: "Doppler (Whetstone)",
22
+ note: "Bankr routes launches through Doppler — a Doppler factory alone does not prove a Bankr launch",
23
+ },
24
+ "0x120caef934797423479fcd0c5d71d36b0282736e": {
25
+ label: "Unverified factory (MemeSoft tokens)",
26
+ note: "Deploys contracts named MemeSoft; operator not yet identified — treat as unbranded",
27
+ },
28
+ };
29
+ // Fallback when the factory address is unknown but its contract is verified and named.
30
+ const LAUNCHPAD_NAME_HINTS = [
31
+ [/pons/i, "Pons"],
32
+ [/doppler/i, "Doppler (Whetstone)"],
33
+ [/bankr/i, "Bankr"],
34
+ [/\bnox\b|noxa/i, "Noxa"],
35
+ [/clanker/i, "Clanker"],
36
+ [/virtuals?/i, "Virtuals"],
37
+ [/flap/i, "Flap"],
38
+ [/memesoft/i, "MemeSoft"],
39
+ ];
40
+ async function detectLaunchpad(creator) {
41
+ if (!creator)
42
+ return null;
43
+ const known = LAUNCHPAD_FACTORIES[creator.toLowerCase()];
44
+ if (known) {
45
+ return { label: known.label, factory: creator, factoryName: null, confidence: "confirmed", note: known.note };
46
+ }
47
+ let isContract = false;
48
+ let factoryName = null;
49
+ try {
50
+ const r = await fetch(`${rh_mcp_constants_js_1.RH_BLOCKSCOUT_V2}/addresses/${creator}`, {
51
+ headers: { accept: "application/json" },
52
+ signal: AbortSignal.timeout(12000),
53
+ });
54
+ if (r.ok) {
55
+ const c = await r.json();
56
+ isContract = c?.is_contract === true;
57
+ factoryName = c?.name ?? null;
58
+ }
59
+ }
60
+ catch {
61
+ /* fall through to unknown */
62
+ }
63
+ if (!isContract) {
64
+ return { label: "None — self-deployed", factory: creator, factoryName: null, confidence: "self-deployed" };
65
+ }
66
+ const hit = factoryName ? LAUNCHPAD_NAME_HINTS.find(([re]) => re.test(factoryName)) : undefined;
67
+ if (hit)
68
+ return { label: hit[1], factory: creator, factoryName, confidence: "likely" };
69
+ return { label: "Unrecognized factory", factory: creator, factoryName, confidence: "unknown" };
70
+ }
71
+ async function blockscoutSafety(address) {
72
+ const out = {
73
+ verified: null,
74
+ contractName: null,
75
+ reputation: null,
76
+ holdersCount: null,
77
+ creator: null,
78
+ launchpad: null,
79
+ };
80
+ try {
81
+ const r = await fetch(`${rh_mcp_constants_js_1.RH_BLOCKSCOUT_V2}/tokens/${address}`, {
82
+ headers: { accept: "application/json" },
83
+ signal: AbortSignal.timeout(12000),
84
+ });
85
+ if (r.ok) {
86
+ const t = await r.json();
87
+ out.reputation = t?.reputation ?? null;
88
+ out.holdersCount = t?.holders_count != null ? Number(t.holders_count) : null;
89
+ }
90
+ }
91
+ catch {
92
+ /* ignore */
93
+ }
94
+ // Use /addresses/{ca} (~1KB) NOT /smart-contracts/{ca} — the latter ships the
95
+ // entire Solidity source and intermittently returns a truncated body on HTTP 200,
96
+ // which silently nulled contractName and skipped the launchpad/privilege flags.
97
+ for (let attempt = 0; attempt < 2; attempt++) {
98
+ try {
99
+ const r = await fetch(`${rh_mcp_constants_js_1.RH_BLOCKSCOUT_V2}/addresses/${address}`, {
100
+ headers: { accept: "application/json" },
101
+ signal: AbortSignal.timeout(12000),
102
+ });
103
+ if (r.ok) {
104
+ const c = await r.json();
105
+ out.verified = c?.is_verified ?? null;
106
+ out.contractName = c?.name ?? null;
107
+ out.creator = c?.creator_address_hash ?? null;
108
+ break;
109
+ }
110
+ if (r.status === 404) {
111
+ out.verified = false; // address has no contract record
112
+ break;
113
+ }
114
+ }
115
+ catch {
116
+ /* retry once */
117
+ }
118
+ }
119
+ out.launchpad = await detectLaunchpad(out.creator);
120
+ return out;
121
+ }
122
+ function assessSafety(safety, dex) {
123
+ let score = 0;
124
+ const flags = [];
125
+ if (safety.verified === false) {
126
+ score += 20;
127
+ flags.push("🔴 Contract NOT verified on explorer");
128
+ }
129
+ else if (safety.verified === true) {
130
+ flags.push("🟢 Contract verified");
131
+ }
132
+ else {
133
+ flags.push("⚪ Verification unknown");
134
+ }
135
+ const rep = (safety.reputation ?? "").toLowerCase();
136
+ if (rep && rep !== "ok" && rep !== "neutral") {
137
+ score += 30;
138
+ flags.push(`🔴 Explorer reputation: "${safety.reputation}"`);
139
+ }
140
+ else if (rep) {
141
+ flags.push(`🟢 Explorer reputation: ${safety.reputation}`);
142
+ }
143
+ if (safety.holdersCount != null) {
144
+ if (safety.holdersCount < 25) {
145
+ score += 25;
146
+ flags.push(`🔴 Very few holders (${safety.holdersCount}) — high concentration / rug risk`);
147
+ }
148
+ else if (safety.holdersCount < 200) {
149
+ score += 12;
150
+ flags.push(`🟠 Few holders (${safety.holdersCount})`);
151
+ }
152
+ else {
153
+ flags.push(`🟢 Holders: ${safety.holdersCount.toLocaleString()}`);
154
+ }
155
+ }
156
+ else {
157
+ flags.push("⚪ Holder count unavailable");
158
+ }
159
+ const nm = (safety.contractName ?? "").toLowerCase();
160
+ if (/mint|burn|blacklist|pausable|pause|ownable|\btax\b|\bfee\b/.test(nm)) {
161
+ score += 14;
162
+ flags.push(`🟠 Contract name implies owner privileges ("${safety.contractName}") — possible mint/burn/blacklist/tax; verify source before trusting`);
163
+ }
164
+ // On Robinhood Chain essentially every token ships through a launchpad
165
+ // (Pons, Bankr/Doppler, Flap, Virtuals…), so a launchpad deploy is the NORM,
166
+ // not a red flag — scoring it as risk penalised every normal token and made
167
+ // the number stop discriminating. The anomaly here is the opposite: a token
168
+ // deployed straight from an EOA answers to no launchpad rules, and the
169
+ // deployer keeps full control of supply and liquidity.
170
+ const lp = safety.launchpad;
171
+ if (lp?.confidence === "confirmed" || lp?.confidence === "likely") {
172
+ flags.push(`🟢 Launched via ${lp.label}${lp.confidence === "likely" ? " (probable)" : ""} — standard for this chain` +
173
+ (lp.note ? ` · ${lp.note}` : ""));
174
+ }
175
+ else if (lp?.confidence === "unknown") {
176
+ score += 10;
177
+ flags.push(`🟠 Deployed by an unrecognized factory (\`${lp.factory}\`) — not a known launchpad on this chain; verify who runs it`);
178
+ }
179
+ else if (lp?.confidence === "self-deployed") {
180
+ score += 18;
181
+ flags.push(`🔴 Self-deployed from an EOA (\`${lp.factory}\`) — no launchpad rules apply. ` +
182
+ `Unusual on this chain: the deployer retains full control over supply and liquidity`);
183
+ }
184
+ if (dex) {
185
+ const buys = dex.txns?.h24?.buys ?? 0;
186
+ const sells = dex.txns?.h24?.sells ?? 0;
187
+ if (buys > 5 && sells === 0) {
188
+ score += 30;
189
+ flags.push(`🔴 Honeypot suspicion: ${buys} buys but 0 sells in 24h — may be unsellable`);
190
+ }
191
+ else if (sells > 0) {
192
+ flags.push(`🟢 Sellable: ${sells} sell(s) in 24h — holders can exit`);
193
+ }
194
+ }
195
+ score = Math.min(100, score);
196
+ const tier = score >= 60 ? "🔴 DANGER" : score >= 35 ? "🟠 CAUTION" : score >= 15 ? "🟡 SOME RISK" : "🟢 LOOKS OK";
197
+ return { score, tier, flags };
198
+ }
199
+ // structuredContent payload for rh_safety_check. `tier` is the assessSafety
200
+ // label with its leading emoji stripped so consumers get a clean enum
201
+ // ("DANGER"/"CAUTION"/"SOME RISK"/"LOOKS OK") - single source, no threshold
202
+ // logic duplicated here.
203
+ function buildRhSafetyStructured(r, safety, assessment) {
204
+ return {
205
+ address: r.address,
206
+ symbol: r.symbol ?? null,
207
+ name: r.name ?? null,
208
+ riskScore: assessment.score,
209
+ tier: assessment.tier.replace(/^\S+\s+/, ""),
210
+ verified: safety.verified,
211
+ contractName: safety.contractName,
212
+ reputation: safety.reputation,
213
+ holdersCount: safety.holdersCount,
214
+ creator: safety.creator,
215
+ launchpad: safety.launchpad,
216
+ flags: assessment.flags,
217
+ };
218
+ }
219
+ function renderSafety(r, safety, assessment) {
220
+ const { score, tier, flags } = assessment;
221
+ return [
222
+ `## 🛡️ RH Safety Check — ${r.symbol}${r.name ? ` (${r.name})` : ""}`,
223
+ `\`${r.address}\` · chain 4663${safety.contractName ? ` · contract \`${safety.contractName}\`` : ""}`,
224
+ "",
225
+ `### Safety: ${tier} — ${score}/100 risk`,
226
+ ...flags.map((f) => `- ${f}`),
227
+ "",
228
+ `**Onchain data only (Blockscout + DexScreener) — no LLM, no paid API.**`,
229
+ `For market/liquidity read run \`rh_analyze\`; for X/narrative add \`web_search\` or \`deep_research\`.`,
230
+ `**Explorer**: ${rh_mcp_constants_js_1.RH_EXPLORER}/token/${r.address}`,
231
+ "",
232
+ `_Heuristic — not a full audit. A verified, high-holder, sellable token can still dump. Verify the CA._`,
233
+ ].join("\n");
234
+ }
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+ // Swap execution engine: quote, approvals (direct + Permit2), broadcast,
3
+ // and receipt confirmation. Real funds move through this file.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.quoteRh = quoteRh;
6
+ exports.ensureDirectApproval = ensureDirectApproval;
7
+ exports.ensureSellApprovals = ensureSellApprovals;
8
+ exports.confirmRhSwap = confirmRhSwap;
9
+ exports.broadcastRhSwap = broadcastRhSwap;
10
+ const ethers_1 = require("ethers");
11
+ const convex_js_1 = require("../convex.js");
12
+ const rh_mcp_constants_js_1 = require("./rh-mcp-constants.js");
13
+ const rh_mcp_provider_js_1 = require("./rh-mcp-provider.js");
14
+ const rh_mcp_resolve_js_1 = require("./rh-mcp-resolve.js");
15
+ async function quoteRh(args) {
16
+ const from = await (0, rh_mcp_resolve_js_1.resolveTokenSmart)(args.fromToken);
17
+ const to = await (0, rh_mcp_resolve_js_1.resolveTokenSmart)(args.toToken);
18
+ if (from.kind === to.kind) {
19
+ throw new Error("RH swaps route ETH ↔ token. Buy a token with ETH, or sell a token for ETH.");
20
+ }
21
+ const sellAmount = (0, rh_mcp_provider_js_1.parseHumanToWei)(args.amount, from.decimals);
22
+ const slippagePct = args.maxSlippagePct ?? 2.0;
23
+ // Same bound defi.ts's SwapSchema already enforces for Base swaps
24
+ // (.positive().max(50)) - this file had no equivalent check anywhere, so a
25
+ // negative or absurd value flowed straight through to the backend as
26
+ // slippageBps, both for direct rh_mcp_swap calls and for every unattended
27
+ // rh_orders_tick execution of a DCA/bracket order created with a bad value.
28
+ if (!(slippagePct > 0) || slippagePct > 50) {
29
+ throw new Error(`maxSlippagePct must be greater than 0 and at most 50 (got ${args.maxSlippagePct}).`);
30
+ }
31
+ const slippageBps = Math.round(slippagePct * 100);
32
+ const result = await (0, convex_js_1.callConvex)("/mcp/rh/quote", "POST", {
33
+ sellToken: from.address,
34
+ buyToken: to.address,
35
+ sellAmount,
36
+ taker: args.taker,
37
+ slippageBps,
38
+ fromSymbol: from.symbol,
39
+ toSymbol: to.symbol,
40
+ }, args.toolName ?? "rh_mcp_estimate");
41
+ if (result.error)
42
+ throw new Error(result.error);
43
+ return { ...result, from, to, sellAmount, slippageBps };
44
+ }
45
+ /** Plain ERC-20 approval for routers that pull via transferFrom (SwapRouter02). */
46
+ async function ensureDirectApproval(wallet, token, spender, amountWei) {
47
+ const provider = await (0, rh_mcp_provider_js_1.rhProviderAsync)();
48
+ const signer = wallet.connect(provider);
49
+ const erc20 = new ethers_1.ethers.Contract(token, [
50
+ "function allowance(address,address) view returns (uint256)",
51
+ "function approve(address,uint256) returns (bool)",
52
+ ], signer);
53
+ const allowance = await erc20.allowance(wallet.address, spender);
54
+ if (allowance >= amountWei)
55
+ return [];
56
+ const tx = await erc20.approve(spender, ethers_1.ethers.MaxUint256);
57
+ await tx.wait();
58
+ return [tx.hash];
59
+ }
60
+ async function ensureSellApprovals(wallet, token, amountWei) {
61
+ const provider = await (0, rh_mcp_provider_js_1.rhProviderAsync)();
62
+ const signer = wallet.connect(provider);
63
+ const hashes = [];
64
+ const erc20 = new ethers_1.ethers.Contract(token, [
65
+ "function allowance(address,address) view returns (uint256)",
66
+ "function approve(address,uint256) returns (bool)",
67
+ ], signer);
68
+ const allowance = await erc20.allowance(wallet.address, rh_mcp_constants_js_1.RH_PERMIT2);
69
+ if (allowance < amountWei) {
70
+ const tx = await erc20.approve(rh_mcp_constants_js_1.RH_PERMIT2, ethers_1.ethers.MaxUint256);
71
+ await tx.wait();
72
+ hashes.push(tx.hash);
73
+ }
74
+ // Permit2.approve(token, spender, amount, expiration) selector 0x87517c45
75
+ const permit2 = new ethers_1.ethers.Contract(rh_mcp_constants_js_1.RH_PERMIT2, [
76
+ "function approve(address token, address spender, uint160 amount, uint48 expiration)",
77
+ "function allowance(address user, address token, address spender) view returns (uint160 amount, uint48 expiration, uint48 nonce)",
78
+ ], signer);
79
+ try {
80
+ const al = await permit2.allowance(wallet.address, token, rh_mcp_constants_js_1.RH_UNIVERSAL_ROUTER);
81
+ const amt = BigInt(al.amount ?? al[0] ?? 0);
82
+ const exp = Number(al.expiration ?? al[1] ?? 0);
83
+ const now = Math.floor(Date.now() / 1000);
84
+ if (amt >= amountWei && exp > now + 60)
85
+ return hashes;
86
+ }
87
+ catch {
88
+ /* re-approve */
89
+ }
90
+ const expiration = Math.floor(Date.now() / 1000) + 30 * 24 * 3600; // 30d
91
+ // uint160 max for amount
92
+ const max160 = (1n << 160n) - 1n;
93
+ const tx2 = await permit2.approve(token, rh_mcp_constants_js_1.RH_UNIVERSAL_ROUTER, max160, expiration);
94
+ await tx2.wait();
95
+ hashes.push(tx2.hash);
96
+ return hashes;
97
+ }
98
+ /**
99
+ * Wait for the swap receipt and read back what actually happened on-chain.
100
+ *
101
+ * A quote is a prediction; a receipt is proof. Reporting only the quoted amount
102
+ * leaves the user unable to tell a real fill from a fabricated one, so we
103
+ * surface the mined status, block, gas, and the ACTUAL amount credited —
104
+ * parsed from the token's Transfer logs (buys) or the ETH balance delta (sells).
105
+ */
106
+ async function confirmRhSwap(txHash, walletAddress, to, ethBefore) {
107
+ try {
108
+ const provider = await (0, rh_mcp_provider_js_1.rhProviderAsync)();
109
+ const receipt = await provider.waitForTransaction(txHash, 1, 90000);
110
+ if (!receipt)
111
+ return { mined: false };
112
+ let received;
113
+ if (to.kind === "token") {
114
+ // Sum Transfer(_, me, value) emitted by the bought token.
115
+ const TRANSFER_TOPIC = ethers_1.ethers.id("Transfer(address,address,uint256)");
116
+ const me = walletAddress.toLowerCase();
117
+ let total = 0n;
118
+ for (const log of receipt.logs ?? []) {
119
+ if (log.address?.toLowerCase() !== to.address.toLowerCase())
120
+ continue;
121
+ if (log.topics?.[0] !== TRANSFER_TOPIC || log.topics.length < 3)
122
+ continue;
123
+ const dest = "0x" + log.topics[2].slice(-40);
124
+ if (dest.toLowerCase() !== me)
125
+ continue;
126
+ try {
127
+ total += BigInt(log.data);
128
+ }
129
+ catch {
130
+ /* skip malformed */
131
+ }
132
+ }
133
+ if (total > 0n)
134
+ received = ethers_1.ethers.formatUnits(total, to.decimals);
135
+ }
136
+ else if (ethBefore != null) {
137
+ // Selling for native ETH: credited amount = delta + gas actually burned.
138
+ try {
139
+ const after = BigInt(await (0, rh_mcp_provider_js_1.rhRpc)("eth_getBalance", [walletAddress, "latest"]));
140
+ const fee = BigInt(receipt.gasUsed ?? 0n) * BigInt(receipt.gasPrice ?? 0n);
141
+ const delta = after - ethBefore + fee;
142
+ if (delta > 0n)
143
+ received = ethers_1.ethers.formatEther(delta);
144
+ }
145
+ catch {
146
+ /* balance read optional */
147
+ }
148
+ }
149
+ return {
150
+ mined: true,
151
+ ok: receipt.status === 1,
152
+ block: receipt.blockNumber,
153
+ gasUsed: receipt.gasUsed?.toString(),
154
+ received,
155
+ };
156
+ }
157
+ catch {
158
+ return { mined: false };
159
+ }
160
+ }
161
+ async function broadcastRhSwap(wallet, tx) {
162
+ const provider = await (0, rh_mcp_provider_js_1.rhProviderAsync)();
163
+ const signer = wallet.connect(provider);
164
+ // "pending" (not "latest") so sequential swaps in one orders-tick get
165
+ // incrementing nonces instead of colliding on the same unmined nonce.
166
+ const nonce = await provider.getTransactionCount(wallet.address, "pending");
167
+ const fee = await provider.getFeeData();
168
+ const signed = await signer.signTransaction({
169
+ to: tx.to,
170
+ data: tx.data,
171
+ value: BigInt(tx.value || "0"),
172
+ gasLimit: BigInt(tx.gas || "650000"),
173
+ maxFeePerGas: fee.maxFeePerGas ?? fee.gasPrice ?? 1000000000n,
174
+ maxPriorityFeePerGas: fee.maxPriorityFeePerGas ?? 100000000n,
175
+ nonce,
176
+ chainId: rh_mcp_constants_js_1.RH_CHAIN_ID,
177
+ type: 2,
178
+ });
179
+ const resp = await (0, rh_mcp_provider_js_1.rhRpc)("eth_sendRawTransaction", [signed]);
180
+ return resp;
181
+ }
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ // Tool definitions for the rh_mcp_* / rh_* namespace. Pure data — no logic.
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.RH_MCP_TOOLS = void 0;
5
+ exports.RH_MCP_TOOLS = [
6
+ {
7
+ name: "rh_mcp_status",
8
+ description: "Robinhood Chain MCP - status of RH rail (chainId 4663): wallet address, RPC, " +
9
+ "ETH gas balance on RH, explorer. Use at the start of any tokenized-stock session. " +
10
+ "NOT Robinhood Agentic brokerage (agent.robinhood.com).",
11
+ inputSchema: { type: "object", properties: {}, required: [] },
12
+ },
13
+ {
14
+ name: "rh_mcp_list_stocks",
15
+ description: "Robinhood Chain MCP - list the 22 Finch/ClawHood tokenized stock tickers " +
16
+ "(symbol, name, contract address) tradeable via Uniswap V4 on chain 4663.",
17
+ inputSchema: {
18
+ type: "object",
19
+ properties: {
20
+ query: {
21
+ type: "string",
22
+ description: "Optional filter: symbol or name substring (e.g. 'NVDA', 'Apple')",
23
+ },
24
+ },
25
+ required: [],
26
+ },
27
+ },
28
+ {
29
+ name: "rh_mcp_balance",
30
+ description: "Robinhood Chain MCP - ETH + tokenized stock balances on RH (chain 4663) for your " +
31
+ "Finch MCP wallet (same address as Base). Reads RH RPC directly (not Alchemy Base).",
32
+ inputSchema: {
33
+ type: "object",
34
+ properties: {
35
+ address: {
36
+ type: "string",
37
+ description: "Optional 0x address (default: local Finch MCP wallet)",
38
+ },
39
+ },
40
+ required: [],
41
+ },
42
+ },
43
+ {
44
+ name: "rh_mcp_estimate",
45
+ description: "Robinhood Chain MCP - preview ETH↔token swap quote via Uniswap V4 (direct or " +
46
+ "multi-hop via USDG). Does NOT execute. Always call before rh_mcp_swap. " +
47
+ "fromToken/toToken: 'ETH' or ANY RH-chain asset — a catalog stock symbol " +
48
+ "(NVDA, AAPL…), a crypto ticker, or a 0x contract address. Non-catalog tickers/CAs " +
49
+ "resolve via DexScreener. Route is always ETH↔token (buy with ETH or sell for ETH).",
50
+ inputSchema: {
51
+ type: "object",
52
+ properties: {
53
+ fromToken: { type: "string", description: "Sell asset: ETH, ticker, or 0x contract address" },
54
+ toToken: { type: "string", description: "Buy asset: ETH, ticker, or 0x contract address" },
55
+ amount: { type: "string", description: "Human amount, e.g. '0.01' ETH or '1000' TOKEN" },
56
+ maxSlippagePct: {
57
+ type: "number",
58
+ description: "Slippage tolerance % (default 2.0; raise for thin crypto pairs)",
59
+ },
60
+ },
61
+ required: ["fromToken", "toToken", "amount"],
62
+ },
63
+ },
64
+ {
65
+ name: "rh_mcp_swap",
66
+ description: "Robinhood Chain MCP - execute ETH↔token swap on chain 4663. Quotes Uniswap V2, V3 and V4 " +
67
+ "and routes through whichever returns the most output. Works for catalog stocks AND " +
68
+ "arbitrary RH-chain crypto (by ticker or 0x address; resolved via DexScreener). Use " +
69
+ "rh_mcp_estimate first — and rh_analyze / rh_safety_check for unknown crypto. " +
70
+ "Buys (ETH→token) are 1 tx. Sells approve automatically, and the approval differs by route: " +
71
+ "V4 uses the 2-step Permit2 flow, V2/V3 use a single ERC-20 approve to SwapRouter02. " +
72
+ "Refuses to broadcast when the quote is far below DexScreener spot (override: acceptBadPrice). " +
73
+ "Reports the mined receipt — status, block, gas and the ACTUAL amount received. " +
74
+ "Gas paid in ETH on RH. NOT brokerage orders — for Robinhood Agentic brokerage use official MCP separately.",
75
+ inputSchema: {
76
+ type: "object",
77
+ properties: {
78
+ fromToken: { type: "string", description: "Sell asset: ETH, ticker, or 0x contract address" },
79
+ toToken: { type: "string", description: "Buy asset: ETH, ticker, or 0x contract address" },
80
+ amount: { type: "string", description: "Human amount" },
81
+ maxSlippagePct: { type: "number", description: "Slippage % (default 2.0)" },
82
+ confirm: {
83
+ type: "boolean",
84
+ description: "Must be true to broadcast. Prevents accidental live swaps.",
85
+ },
86
+ acceptBadPrice: {
87
+ type: "boolean",
88
+ description: "Override the spot-price safety stop. Only set if you deliberately accept " +
89
+ "receiving far less than DexScreener spot value (near-empty V4 pool).",
90
+ },
91
+ },
92
+ required: ["fromToken", "toToken", "amount", "confirm"],
93
+ },
94
+ },
95
+ {
96
+ name: "rh_token_resolve",
97
+ description: "Robinhood Chain MCP - resolve a crypto ticker OR 0x contract address to a tradeable " +
98
+ "token on chain 4663 via DexScreener. Ticker search returns candidates ranked by " +
99
+ "liquidity (tickers can be spoofed — always trade by the confirmed contract address). " +
100
+ "Use before rh_mcp_estimate/rh_mcp_swap for non-catalog crypto.",
101
+ inputSchema: {
102
+ type: "object",
103
+ properties: {
104
+ query: { type: "string", description: "Crypto ticker (e.g. 'PEPE') or 0x contract address" },
105
+ },
106
+ required: ["query"],
107
+ },
108
+ },
109
+ {
110
+ name: "rh_analyze",
111
+ description: "Robinhood Chain MCP - market + risk pre-screen for any RH-chain token (ticker or 0x " +
112
+ "address). Pulls DexScreener data (price, liquidity, 24h volume, buy/sell txns, pair age, " +
113
+ "FDV/MCap) and returns a 0-100 risk score with reasoning flags. Combine with rh_safety_check " +
114
+ "(onchain) and deep_research (X sentiment) for a full verdict. Not financial advice.",
115
+ inputSchema: {
116
+ type: "object",
117
+ properties: {
118
+ token: { type: "string", description: "Crypto ticker or 0x contract address on RH chain 4663" },
119
+ },
120
+ required: ["token"],
121
+ },
122
+ },
123
+ {
124
+ name: "rh_safety_check",
125
+ description: "Robinhood Chain MCP - free onchain safety scan for a token (ticker or 0x address). Reads " +
126
+ "Blockscout (contract verified?, explorer reputation, holder count, launchpad-style contract " +
127
+ "name) + DexScreener (sellable? honeypot signal from buys-with-0-sells). No LLM, no paid API. " +
128
+ "Returns red/yellow/green safety flags + a risk score. Pair with rh_analyze (market/liquidity).",
129
+ inputSchema: {
130
+ type: "object",
131
+ properties: {
132
+ token: { type: "string", description: "Crypto ticker or 0x contract address on RH chain 4663" },
133
+ },
134
+ required: ["token"],
135
+ },
136
+ },
137
+ ];