@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 +71 -16
- package/dist/server.js +21 -16
- package/dist/tools/agents.js +79 -0
- package/dist/tools/automation.js +38 -0
- package/dist/tools/base.js +261 -0
- package/dist/tools/coder.js +372 -0
- package/dist/tools/defi.js +106 -1
- package/dist/tools/humanizer.js +22 -15
- package/dist/tools/insight.js +43 -2
- package/dist/tools/scanner.js +375 -0
- package/package.json +1 -1
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
|
25
|
-
|
|
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
|
-
|
|
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.
|
|
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 —
|
|
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, //
|
|
39
|
-
...automation_js_1.AUTOMATION_TOOLS, //
|
|
40
|
-
...swarm_js_1.SWARM_TOOLS, // 6 — start, stop, status, memory, scores
|
|
39
|
+
...defi_js_1.DEFI_TOOLS, // 5 — get_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 —
|
|
44
|
-
...
|
|
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, // 3 — simulate, status, stop
|
|
46
46
|
...humanizer_js_1.HUMANIZER_TOOLS, // 1 — humanize_text
|
|
47
|
-
//
|
|
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
|
+
}
|
package/dist/tools/automation.js
CHANGED
|
@@ -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
|
+
}
|