@noelclaw/mcp 1.5.6 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,29 +3,84 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const server_js_1 = require("./server.js");
5
5
  const wallet_js_1 = require("./wallet.js");
6
- const BANNER = `
7
- \x1b[36m
8
- ███╗ ██╗ ██████╗ ███████╗██╗ ██████╗██╗ █████╗ ██╗ ██╗
9
- ████╗ ██║██╔═══██╗██╔════╝██║ ██╔════╝██║ ██╔══██╗██║ ██║
10
- ██╔██╗ ██║██║ ██║█████╗ ██║ ██║ ██║ ███████║██║ █╗ ██║
11
- ██║╚██╗██║██║ ██║██╔══╝ ██║ ██║ ██║ ██╔══██║██║███╗██║
12
- ██║ ╚████║╚██████╔╝███████╗███████╗╚██████╗███████╗██║ ██║╚███╔███╔╝
13
- ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝
14
- \x1b[0m \x1b[90mcrypto AI · MCP · DeFi · research · swarm\x1b[0m
15
- `;
6
+ // ── ANSI helpers ──────────────────────────────────────────────────────────────
7
+ const C = {
8
+ cyan: "\x1b[36m",
9
+ dim: "\x1b[90m",
10
+ white: "\x1b[97m",
11
+ green: "\x1b[32m",
12
+ yellow: "\x1b[33m",
13
+ reset: "\x1b[0m",
14
+ bold: "\x1b[1m",
15
+ };
16
+ const BANNER = `
17
+ ${C.cyan}
18
+ ███╗ ██╗ ██████╗ ███████╗██╗ ██████╗██╗ █████╗ ██╗ ██╗
19
+ ████╗ ██║██╔═══██╗██╔════╝██║ ██╔════╝██║ ██╔══██╗██║ ██║
20
+ ██╔██╗ ██║██║ ██║█████╗ ██║ ██║ ██║ ███████║██║ █╗ ██║
21
+ ██║╚██╗██║██║ ██║██╔══╝ ██║ ██║ ██║ ██╔══██║██║███╗██║
22
+ ██║ ╚████║╚██████╔╝███████╗███████╗╚██████╗███████╗██║ ██║╚███╔███╔╝
23
+ ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝
24
+ ${C.reset}`;
25
+ function line(label, value, color = C.cyan) {
26
+ const pad = " ".repeat(Math.max(0, 12 - label.length));
27
+ process.stderr.write(` ${color}◆ ${label}${C.reset}${pad}${value}\n`);
28
+ }
29
+ function divider() {
30
+ process.stderr.write(` ${C.dim}${"─".repeat(58)}${C.reset}\n`);
31
+ }
16
32
  async function main() {
17
- console.error(BANNER);
33
+ process.stderr.write(BANNER);
34
+ // ── Tool category counts ──────────────────────────────────────────────────
35
+ const categories = [
36
+ { label: "Market", count: 3, tools: "get_market_data · get_token_data · ask_noel" },
37
+ { label: "DeFi", count: 5, tools: "portfolio · swap · send · scan_wallet · estimate" },
38
+ { label: "Automation", count: 5, tools: "create · list · pause · delete · runs" },
39
+ { label: "Scanner", count: 3, tools: "scan_dips · score_token · check_token" },
40
+ { label: "Agents", count: 2, tools: "list_agents · hire_agent" },
41
+ { label: "Swarm", count: 6, tools: "start · stop · status · read_memory · write_memory · scores" },
42
+ { label: "Framework", count: 6, tools: "create_task · list_tasks · list_playbooks · run_playbook · ledger · sentinel" },
43
+ { label: "Vault", count: 7, tools: "save · read · list · search · history · diff · export" },
44
+ { label: "MiroShark", count: 3, tools: "simulate · status · stop" },
45
+ { label: "Wallet", count: 2, tools: "get_wallet_address · set_telegram" },
46
+ { label: "Social", count: 1, tools: "humanize_text" },
47
+ { label: "Coder", count: 6, tools: "scaffold_project · generate_component · generate_contract · audit_contract · explain_code · review_code" },
48
+ { label: "Base", count: 4, tools: "query_vaults · list_markets · prepare_deposit · chain_stats" },
49
+ ];
50
+ const total = server_js_1.ALL_TOOLS.length;
51
+ divider();
52
+ process.stderr.write(`\n`);
53
+ line("version", `v2.0.0 ${C.dim}MCP protocol 2.1.0${C.reset}`);
54
+ line("network", `Base mainnet ${C.dim}via 0x Protocol · ethers v6${C.reset}`);
55
+ line("ai", `Bankr LLM ${C.dim}grok-3 · llm.bankr.bot${C.reset}`);
56
+ line("tools", `${C.white}${C.bold}${total} tools loaded${C.reset} ${C.dim}across ${categories.length} categories${C.reset}`);
57
+ process.stderr.write(`\n`);
58
+ divider();
59
+ process.stderr.write(`\n`);
60
+ // ── Categories grid ───────────────────────────────────────────────────────
61
+ for (const cat of categories) {
62
+ const countStr = `${cat.count}`.padStart(2);
63
+ process.stderr.write(` ${C.dim}│${C.reset} ${C.cyan}${cat.label.padEnd(11)}${C.reset} ${C.dim}${countStr}x${C.reset} ${C.dim}${cat.tools}${C.reset}\n`);
64
+ }
65
+ process.stderr.write(`\n`);
66
+ divider();
67
+ // ── Wallet ────────────────────────────────────────────────────────────────
18
68
  await (0, server_js_1.startServer)();
19
69
  try {
20
70
  const wallet = await (0, wallet_js_1.getOrCreateWallet)();
21
- console.error(`\x1b[36m ◆ wallet\x1b[0m ${wallet.address}`);
22
- console.error(`\x1b[36m ◆ status\x1b[0m ready · 34 tools loaded\n`);
71
+ process.stderr.write(`\n`);
72
+ line("wallet", wallet.address);
73
+ line("status", `${C.green}ready${C.reset} ${C.dim}waiting for MCP client...${C.reset}`, C.green);
74
+ process.stderr.write(`\n`);
23
75
  }
24
- catch (err) {
25
- console.error(`[noelclaw] wallet init failed: ${err}`);
76
+ catch {
77
+ process.stderr.write(`\n`);
78
+ line("wallet", `${C.yellow}not configured${C.reset} ${C.dim}run 'noelclaw-mcp' to init${C.reset}`, C.yellow);
79
+ line("status", `${C.green}ready${C.reset} ${C.dim}wallet tools require setup${C.reset}`, C.green);
80
+ process.stderr.write(`\n`);
26
81
  }
27
82
  }
28
83
  main().catch((err) => {
29
- console.error(err);
84
+ process.stderr.write(`[noelclaw] fatal: ${err}\n`);
30
85
  process.exit(1);
31
86
  });
package/dist/server.js CHANGED
@@ -7,22 +7,23 @@ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
7
7
  const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
8
8
  const convex_js_1 = require("./convex.js");
9
9
  const market_js_1 = require("./tools/market.js");
10
- const research_js_1 = require("./tools/research.js");
11
10
  const defi_js_1 = require("./tools/defi.js");
12
11
  const automation_js_1 = require("./tools/automation.js");
13
12
  const swarm_js_1 = require("./tools/swarm.js");
14
13
  const insight_js_1 = require("./tools/insight.js");
15
14
  const framework_js_1 = require("./tools/framework.js");
16
15
  const wallet_js_1 = require("./tools/wallet.js");
17
- const news_js_1 = require("./tools/news.js");
18
16
  const vault_js_1 = require("./tools/vault.js");
19
- const twitter_js_1 = require("./tools/twitter.js");
20
17
  const miroshark_js_1 = require("./tools/miroshark.js");
21
18
  const humanizer_js_1 = require("./tools/humanizer.js");
19
+ const agents_js_1 = require("./tools/agents.js");
20
+ const scanner_js_1 = require("./tools/scanner.js");
21
+ const coder_js_1 = require("./tools/coder.js");
22
+ const base_js_1 = require("./tools/base.js");
22
23
  const PRIVATE_KEY_RESPONSE = {
23
24
  content: [{
24
25
  type: "text",
25
- text: "I don't have access to your private key. Your wallet is secured by Noelclaw's encrypted vault. Only you can manage it at noelclaw.xyz",
26
+ text: "I don't have access to your private key. Your wallet is secured by Noelclaw's encrypted vault. Only you can manage it at noelclaw.com",
26
27
  }],
27
28
  };
28
29
  function containsSensitiveRequest(args) {
@@ -33,18 +34,21 @@ function containsSensitiveRequest(args) {
33
34
  text.includes("privatekey"));
34
35
  }
35
36
  exports.ALL_TOOLS = [
36
- ...market_js_1.MARKET_TOOLS, // 2 — market data, token data
37
+ ...market_js_1.MARKET_TOOLS, // 2 — get_market_data, get_token_data
37
38
  ...insight_js_1.INSIGHT_TOOLS, // 1 — ask_noel
38
- ...defi_js_1.DEFI_TOOLS, // 2swap, send
39
- ...automation_js_1.AUTOMATION_TOOLS, // 4 — create, list, pause, delete
40
- ...swarm_js_1.SWARM_TOOLS, // 6 — start, stop, status, memory, scores
39
+ ...defi_js_1.DEFI_TOOLS, // 5get_portfolio, estimate_swap, swap_tokens, send_token, scan_wallet
40
+ ...automation_js_1.AUTOMATION_TOOLS, // 5 — create, list, pause, delete, get_runs
41
+ ...swarm_js_1.SWARM_TOOLS, // 6 — start, stop, status, read/write memory, scores
41
42
  ...framework_js_1.FRAMEWORK_TOOLS, // 6 — task packets, playbooks, sentinel, ledger
42
43
  ...vault_js_1.VAULT_TOOLS, // 7 — save, read, list, search, history, diff, export
43
- ...wallet_js_1.WALLET_TOOLS, // 2 — wallet address, telegram connect
44
- ...twitter_js_1.TWITTER_TOOLS, // 1post tweet
45
- ...miroshark_js_1.MIROSHARK_TOOLS, // 2 — simulate, status
44
+ ...wallet_js_1.WALLET_TOOLS, // 2 — get_wallet_address, set_telegram
45
+ ...miroshark_js_1.MIROSHARK_TOOLS, // 3simulate, status, stop
46
46
  ...humanizer_js_1.HUMANIZER_TOOLS, // 1 — humanize_text
47
- // total: 34
47
+ ...agents_js_1.AGENT_TOOLS, // 2 — list_agents, hire_agent
48
+ ...scanner_js_1.SCANNER_TOOLS, // 3 — score_token, check_token, scan_dips
49
+ ...coder_js_1.CODER_TOOLS, // 6 — scaffold_project, generate_component, generate_contract, audit_contract, explain_code, review_code
50
+ ...base_js_1.BASE_TOOLS, // 4 — query_vaults, list_markets, prepare_deposit, chain_stats
51
+ // total: 53
48
52
  ];
49
53
  exports.server = new index_js_1.Server({ name: "noelclaw", version: "2.1.0" }, { capabilities: { tools: {} } });
50
54
  exports.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({ tools: exports.ALL_TOOLS }));
@@ -54,8 +58,6 @@ exports.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (reques
54
58
  return PRIVATE_KEY_RESPONSE;
55
59
  try {
56
60
  const result = await (0, market_js_1.handleMarketTool)(name, args) ??
57
- await (0, news_js_1.handleNewsTool)(name, args) ??
58
- await (0, research_js_1.handleResearchTool)(name, args) ??
59
61
  await (0, defi_js_1.handleDefiTool)(name, args) ??
60
62
  await (0, automation_js_1.handleAutomationTool)(name, args) ??
61
63
  await (0, swarm_js_1.handleSwarmTool)(name, args) ??
@@ -63,9 +65,12 @@ exports.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (reques
63
65
  await (0, vault_js_1.handleVaultTool)(name, args) ??
64
66
  await (0, wallet_js_1.handleWalletTool)(name, args) ??
65
67
  await (0, insight_js_1.handleInsightTool)(name, args) ??
66
- await (0, twitter_js_1.handleTwitterTool)(name, args) ??
67
68
  await (0, miroshark_js_1.handleMirosharkTool)(name, args) ??
68
- await (0, humanizer_js_1.handleHumanizerTool)(name, args);
69
+ await (0, humanizer_js_1.handleHumanizerTool)(name, args) ??
70
+ await (0, agents_js_1.handleAgentTool)(name, args) ??
71
+ await (0, scanner_js_1.handleScannerTool)(name, args) ??
72
+ await (0, coder_js_1.handleCoderTool)(name, args) ??
73
+ await (0, base_js_1.handleBaseTool)(name, args);
69
74
  if (result)
70
75
  return result;
71
76
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AGENT_TOOLS = void 0;
4
+ exports.handleAgentTool = handleAgentTool;
5
+ const zod_1 = require("zod");
6
+ const convex_js_1 = require("../convex.js");
7
+ exports.AGENT_TOOLS = [
8
+ {
9
+ name: "list_agents",
10
+ description: "List all available specialist agents you can hire — built-in experts (analyst, risk-manager, researcher, executor, scout) plus any community-published agents.",
11
+ inputSchema: { type: "object", properties: {}, required: [] },
12
+ },
13
+ {
14
+ name: "hire_agent",
15
+ description: "Hire a specialist agent to complete a task. The agent runs immediately with its own expertise and returns a focused analysis or execution plan. Use list_agents first to see what's available.",
16
+ inputSchema: {
17
+ type: "object",
18
+ properties: {
19
+ agentId: {
20
+ type: "string",
21
+ description: "Agent ID from list_agents. Built-in: analyst, risk-manager, researcher, executor, scout. Or a custom agent ID.",
22
+ },
23
+ task: {
24
+ type: "string",
25
+ description: "The task or question for the agent. Be specific — better input = better output.",
26
+ },
27
+ maxTokens: {
28
+ type: "number",
29
+ description: "Max response tokens (default 800, max 1200). Lower is faster and cheaper.",
30
+ },
31
+ },
32
+ required: ["agentId", "task"],
33
+ },
34
+ },
35
+ ];
36
+ const HireAgentSchema = zod_1.z.object({
37
+ agentId: zod_1.z.string().min(1),
38
+ task: zod_1.z.string().min(1),
39
+ maxTokens: zod_1.z.number().int().min(100).max(1200).optional(),
40
+ });
41
+ async function handleAgentTool(name, args) {
42
+ if (name === "list_agents") {
43
+ const data = await (0, convex_js_1.callConvex)("/agents/list", "GET", undefined, "list_agents");
44
+ const agents = data.agents ?? [];
45
+ const lines = agents.map((a) => {
46
+ const badge = a.pricingType === "free" ? "free" : "token-based";
47
+ const runs = a.runs != null ? ` · ${a.runs} runs` : "";
48
+ return `**${a.name}** (\`${a.id}\`) [${badge}${runs}]\n ${a.description}`;
49
+ });
50
+ return {
51
+ content: [{
52
+ type: "text",
53
+ text: `## Available Agents (${agents.length})\n\n${lines.join("\n\n")}`,
54
+ }],
55
+ };
56
+ }
57
+ if (name === "hire_agent") {
58
+ const parsed = HireAgentSchema.safeParse(args);
59
+ if (!parsed.success) {
60
+ return {
61
+ content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }],
62
+ isError: true,
63
+ };
64
+ }
65
+ const { agentId, task, maxTokens } = parsed.data;
66
+ const data = await (0, convex_js_1.callConvex)("/agents/hire", "POST", { agentId, task, maxTokens }, "hire_agent");
67
+ if (data.error) {
68
+ return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
69
+ }
70
+ const footer = data.tokensUsed ? `\n\n*Tokens used: ${data.tokensUsed}*` : "";
71
+ return {
72
+ content: [{
73
+ type: "text",
74
+ text: `## ${data.agent ?? agentId} — Response\n\n${data.result}${footer}`,
75
+ }],
76
+ };
77
+ }
78
+ return null;
79
+ }
@@ -37,9 +37,22 @@ exports.AUTOMATION_TOOLS = [
37
37
  required: ["automationId"],
38
38
  },
39
39
  },
40
+ {
41
+ name: "get_automation_runs",
42
+ description: "Get the execution history for an automation — each run's status (success/failed/skipped), amount spent, tx hash, and error message if any. Useful for debugging why an automation isn't working.",
43
+ inputSchema: {
44
+ type: "object",
45
+ properties: {
46
+ automationId: { type: "string", description: "Automation ID (from list_automations)" },
47
+ limit: { type: "number", description: "Max runs to return (default 20)" },
48
+ },
49
+ required: ["automationId"],
50
+ },
51
+ },
40
52
  ];
41
53
  const CreateAutomationSchema = zod_1.z.object({ rawInput: zod_1.z.string().min(1) });
42
54
  const AutomationIdSchema = zod_1.z.object({ automationId: zod_1.z.string().min(1) });
55
+ const RunsSchema = zod_1.z.object({ automationId: zod_1.z.string().min(1), limit: zod_1.z.number().int().min(1).max(100).optional() });
43
56
  async function handleAutomationTool(name, args) {
44
57
  switch (name) {
45
58
  case "create_automation": {
@@ -110,6 +123,31 @@ async function handleAutomationTool(name, args) {
110
123
  return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
111
124
  return { content: [{ type: "text", text: "🗑️ Automation deleted." }] };
112
125
  }
126
+ case "get_automation_runs": {
127
+ const parsed = RunsSchema.safeParse(args);
128
+ if (!parsed.success)
129
+ return { content: [{ type: "text", text: `Invalid input: automationId ${parsed.error.issues[0].message}` }], isError: true };
130
+ const { automationId, limit = 20 } = parsed.data;
131
+ const qs = `automationId=${encodeURIComponent(automationId)}&limit=${limit}`;
132
+ const data = await (0, convex_js_1.callConvex)(`/automations/runs?${qs}`, "GET", undefined, "get_automation_runs");
133
+ if (data.error)
134
+ return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
135
+ const runs = data.runs ?? [];
136
+ if (!runs.length)
137
+ return { content: [{ type: "text", text: "No runs yet for this automation." }] };
138
+ const statusIcon = { success: "✅", failed: "❌", skipped: "⏭️" };
139
+ const lines = [`**Run History** (${runs.length} shown)`, ""];
140
+ for (const r of runs) {
141
+ const icon = statusIcon[r.status] ?? "•";
142
+ const time = new Date(r.triggeredAt).toUTCString();
143
+ const spent = r.amountUsd != null ? ` · $${Number(r.amountUsd).toFixed(2)}` : "";
144
+ const tx = r.txHash ? ` · [tx](https://basescan.org/tx/${r.txHash})` : "";
145
+ lines.push(`${icon} **${r.status}**${spent}${tx} — ${time}`);
146
+ if (r.error)
147
+ lines.push(` ⚠️ ${r.error}`);
148
+ }
149
+ return { content: [{ type: "text", text: lines.join("\n") }] };
150
+ }
113
151
  default:
114
152
  return null;
115
153
  }
@@ -0,0 +1,261 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BASE_TOOLS = void 0;
4
+ exports.handleBaseTool = handleBaseTool;
5
+ const MORPHO_API = "https://blue-api.morpho.org/graphql";
6
+ const MOONWELL_API = "https://api.moonwell.fi/v1/markets";
7
+ exports.BASE_TOOLS = [
8
+ {
9
+ name: "base_query_vaults",
10
+ description: "List Morpho yield vaults on Base sorted by APY. Shows vault name, asset, current APY, and total deposits. Use this to find the best yield opportunities on Base.",
11
+ inputSchema: {
12
+ type: "object",
13
+ properties: {
14
+ asset: {
15
+ type: "string",
16
+ description: "Filter by asset symbol (e.g. USDC, WETH, cbBTC). Leave empty for all.",
17
+ },
18
+ limit: {
19
+ type: "number",
20
+ description: "Max vaults to return (default 10)",
21
+ },
22
+ },
23
+ required: [],
24
+ },
25
+ },
26
+ {
27
+ name: "base_list_markets",
28
+ description: "List Moonwell lending/borrowing markets on Base. Shows supply APY, borrow APY, total liquidity, and utilization rate for each asset.",
29
+ inputSchema: {
30
+ type: "object",
31
+ properties: {
32
+ asset: {
33
+ type: "string",
34
+ description: "Filter by asset symbol (e.g. USDC, ETH, cbBTC). Leave empty for all.",
35
+ },
36
+ },
37
+ required: [],
38
+ },
39
+ },
40
+ {
41
+ name: "base_prepare_deposit",
42
+ description: "Get deposit instructions for a Morpho vault — shows the vault address, expected APY, and step-by-step instructions. Does NOT execute the transaction.",
43
+ inputSchema: {
44
+ type: "object",
45
+ properties: {
46
+ vaultName: {
47
+ type: "string",
48
+ description: "Name or partial name of the vault (e.g. 'Gauntlet USDC', 'steakUSDC')",
49
+ },
50
+ amount: {
51
+ type: "string",
52
+ description: "Amount to deposit (e.g. '100', '1000')",
53
+ },
54
+ asset: {
55
+ type: "string",
56
+ description: "Asset to deposit (e.g. USDC, WETH)",
57
+ },
58
+ },
59
+ required: ["asset", "amount"],
60
+ },
61
+ },
62
+ {
63
+ name: "base_chain_stats",
64
+ description: "Get real-time Base chain stats: ETH price, gas price in gwei, and latest block info.",
65
+ inputSchema: {
66
+ type: "object",
67
+ properties: {},
68
+ required: [],
69
+ },
70
+ },
71
+ ];
72
+ // ── Helpers ────────────────────────────────────────────────────────────────────
73
+ function fmt(n, decimals = 2) {
74
+ if (n >= 1000000000)
75
+ return `$${(n / 1000000000).toFixed(1)}B`;
76
+ if (n >= 1000000)
77
+ return `$${(n / 1000000).toFixed(1)}M`;
78
+ if (n >= 1000)
79
+ return `$${(n / 1000).toFixed(1)}K`;
80
+ return `$${n.toFixed(decimals)}`;
81
+ }
82
+ function pct(n) {
83
+ return `${(n * 100).toFixed(2)}%`;
84
+ }
85
+ // ── Morpho vaults ─────────────────────────────────────────────────────────────
86
+ async function fetchMorphoVaults(asset, limit = 10) {
87
+ const gql = `{
88
+ vaults(
89
+ where: { chainId_in: [8453] }
90
+ orderBy: "state_netApy"
91
+ orderDirection: "desc"
92
+ first: 50
93
+ ) {
94
+ items {
95
+ name
96
+ address
97
+ asset { symbol name }
98
+ state { apy netApy totalAssetsUsd }
99
+ }
100
+ }
101
+ }`;
102
+ const res = await fetch(MORPHO_API, {
103
+ method: "POST",
104
+ headers: { "Content-Type": "application/json" },
105
+ body: JSON.stringify({ query: gql }),
106
+ signal: AbortSignal.timeout(15000),
107
+ });
108
+ if (!res.ok)
109
+ throw new Error(`Morpho API error: ${res.status}`);
110
+ const data = await res.json();
111
+ let vaults = data?.data?.vaults?.items ?? [];
112
+ if (asset) {
113
+ vaults = vaults.filter((v) => v.asset?.symbol?.toLowerCase().includes(asset.toLowerCase()));
114
+ }
115
+ vaults = vaults.slice(0, limit);
116
+ if (!vaults.length)
117
+ return "No vaults found for that asset.";
118
+ const lines = vaults.map((v, i) => {
119
+ const apy = pct(v.state?.netApy ?? v.state?.apy ?? 0);
120
+ const tvl = fmt(v.state?.totalAssetsUsd ?? 0);
121
+ const addr = `${v.address?.slice(0, 6)}...${v.address?.slice(-4)}`;
122
+ return `${i + 1}. ${v.name}\n Asset: ${v.asset?.symbol} APY: ${apy} TVL: ${tvl}\n Address: ${addr}`;
123
+ });
124
+ return `Morpho Vaults on Base (sorted by APY):\n\n${lines.join("\n\n")}`;
125
+ }
126
+ // ── Moonwell markets ───────────────────────────────────────────────────────────
127
+ async function fetchMoonwellMarkets(asset) {
128
+ const res = await fetch(`${MOONWELL_API}?network=base`, {
129
+ signal: AbortSignal.timeout(15000),
130
+ });
131
+ if (!res.ok)
132
+ throw new Error(`Moonwell API error: ${res.status}`);
133
+ const data = await res.json();
134
+ let markets = data?.data ?? data?.markets ?? [];
135
+ if (asset) {
136
+ markets = markets.filter((m) => (m.underlyingSymbol ?? m.symbol ?? "").toLowerCase().includes(asset.toLowerCase()));
137
+ }
138
+ if (!markets.length)
139
+ return "No markets found.";
140
+ const lines = markets.slice(0, 15).map((m, i) => {
141
+ const symbol = m.underlyingSymbol ?? m.symbol ?? "?";
142
+ const supplyApy = pct((m.supplyApy ?? m.supplyRate ?? 0));
143
+ const borrowApy = pct((m.borrowApy ?? m.borrowRate ?? 0));
144
+ const liquidity = fmt(m.totalSupplyUsd ?? m.totalSupply ?? 0);
145
+ const util = m.utilization != null ? `${(m.utilization * 100).toFixed(1)}%` : "—";
146
+ return `${i + 1}. ${symbol}\n Supply APY: ${supplyApy} Borrow APY: ${borrowApy} Liquidity: ${liquidity} Util: ${util}`;
147
+ });
148
+ return `Moonwell Markets on Base:\n\n${lines.join("\n\n")}`;
149
+ }
150
+ // ── Base chain stats ───────────────────────────────────────────────────────────
151
+ async function fetchBaseStats() {
152
+ const rpc = "https://mainnet.base.org";
153
+ const [blockRes, priceRes, gasRes] = await Promise.all([
154
+ fetch(rpc, {
155
+ method: "POST",
156
+ headers: { "Content-Type": "application/json" },
157
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_blockNumber", params: [] }),
158
+ signal: AbortSignal.timeout(8000),
159
+ }).then(r => r.json()).catch(() => null),
160
+ fetch("https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd", {
161
+ signal: AbortSignal.timeout(8000),
162
+ }).then(r => r.json()).catch(() => null),
163
+ fetch(rpc, {
164
+ method: "POST",
165
+ headers: { "Content-Type": "application/json" },
166
+ body: JSON.stringify({ jsonrpc: "2.0", id: 2, method: "eth_gasPrice", params: [] }),
167
+ signal: AbortSignal.timeout(8000),
168
+ }).then(r => r.json()).catch(() => null),
169
+ ]);
170
+ const block = blockRes?.result ? parseInt(blockRes.result, 16) : "—";
171
+ const ethPrice = priceRes?.ethereum?.usd ?? "—";
172
+ const gasPriceGwei = gasRes?.result
173
+ ? (parseInt(gasRes.result, 16) / 1e9).toFixed(4)
174
+ : "—";
175
+ return `Base Chain Stats:\n\n• ETH Price: $${ethPrice}\n• Gas Price: ${gasPriceGwei} gwei\n• Latest Block: ${block.toLocaleString()}\n• Network: Base Mainnet (Chain ID 8453)`;
176
+ }
177
+ // ── Prepare deposit info ───────────────────────────────────────────────────────
178
+ async function prepareDeposit(vaultName, asset, amount) {
179
+ const gql = `{
180
+ vaults(
181
+ where: { chainId_in: [8453] }
182
+ orderBy: "state_netApy"
183
+ orderDirection: "desc"
184
+ first: 100
185
+ ) {
186
+ items {
187
+ name
188
+ address
189
+ asset { symbol name }
190
+ state { netApy apy totalAssetsUsd }
191
+ }
192
+ }
193
+ }`;
194
+ const res = await fetch(MORPHO_API, {
195
+ method: "POST",
196
+ headers: { "Content-Type": "application/json" },
197
+ body: JSON.stringify({ query: gql }),
198
+ signal: AbortSignal.timeout(15000),
199
+ });
200
+ if (!res.ok)
201
+ throw new Error(`Morpho API error: ${res.status}`);
202
+ const data = await res.json();
203
+ let vaults = data?.data?.vaults?.items ?? [];
204
+ // Filter by asset first
205
+ vaults = vaults.filter((v) => v.asset?.symbol?.toLowerCase() === asset.toLowerCase());
206
+ // Filter by vault name if provided
207
+ if (vaultName) {
208
+ const match = vaults.find((v) => v.name?.toLowerCase().includes(vaultName.toLowerCase()));
209
+ if (match)
210
+ vaults = [match];
211
+ }
212
+ // Take best APY vault
213
+ const vault = vaults[0];
214
+ if (!vault) {
215
+ return `No Morpho vault found for ${asset} on Base. Try base_query_vaults to see available vaults.`;
216
+ }
217
+ const apy = pct(vault.state?.netApy ?? vault.state?.apy ?? 0);
218
+ const tvl = fmt(vault.state?.totalAssetsUsd ?? 0);
219
+ return [
220
+ `Morpho Vault Deposit Instructions`,
221
+ ``,
222
+ `Vault: ${vault.name}`,
223
+ `Asset: ${vault.asset?.symbol}`,
224
+ `APY: ${apy} | TVL: ${tvl}`,
225
+ `Contract: ${vault.address}`,
226
+ ``,
227
+ `Steps to deposit ${amount} ${asset}:`,
228
+ `1. Go to app.morpho.org or use the vault address above`,
229
+ `2. Connect your wallet (ensure you have ${amount} ${asset})`,
230
+ `3. Approve the vault contract to spend your ${asset}`,
231
+ `4. Call deposit(${amount}, yourAddress) on the vault contract`,
232
+ `5. You'll receive vault shares representing your deposit`,
233
+ ``,
234
+ `Expected yield: ~${apy} on ${amount} ${asset}`,
235
+ `Note: APY is variable and changes based on market conditions.`,
236
+ ].join("\n");
237
+ }
238
+ // ── Handler ────────────────────────────────────────────────────────────────────
239
+ async function handleBaseTool(name, args) {
240
+ const a = (args ?? {});
241
+ switch (name) {
242
+ case "base_query_vaults": {
243
+ const text = await fetchMorphoVaults(a.asset, a.limit ?? 10);
244
+ return { content: [{ type: "text", text }] };
245
+ }
246
+ case "base_list_markets": {
247
+ const text = await fetchMoonwellMarkets(a.asset);
248
+ return { content: [{ type: "text", text }] };
249
+ }
250
+ case "base_prepare_deposit": {
251
+ const text = await prepareDeposit(a.vaultName, a.asset, a.amount);
252
+ return { content: [{ type: "text", text }] };
253
+ }
254
+ case "base_chain_stats": {
255
+ const text = await fetchBaseStats();
256
+ return { content: [{ type: "text", text }] };
257
+ }
258
+ default:
259
+ return null;
260
+ }
261
+ }