@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
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CODER_TOOLS = void 0;
|
|
4
|
+
exports.handleCoderTool = handleCoderTool;
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const BANKR_LLM_URL = "https://llm.bankr.bot/v1/chat/completions";
|
|
7
|
+
const BANKR_MODEL = process.env.BANKR_MODEL ?? "grok-3";
|
|
8
|
+
const CODER_SYSTEM = `You are Noel Coder — an expert software engineer embedded in the Noelclaw AI OS on Base chain.
|
|
9
|
+
You specialize in:
|
|
10
|
+
- Solidity smart contracts (ERC-20, ERC-721, DeFi hooks, Uniswap v3/v4 integrations)
|
|
11
|
+
- TypeScript / React / Next.js frontend development
|
|
12
|
+
- Convex backend (functions, schema, mutations, queries)
|
|
13
|
+
- MCP (Model Context Protocol) server tools in TypeScript
|
|
14
|
+
- Base chain ecosystem: 0x Protocol, Uniswap v3, ethers v6, wagmi v2
|
|
15
|
+
|
|
16
|
+
Rules:
|
|
17
|
+
- Output clean, production-ready code. No placeholders.
|
|
18
|
+
- Add brief inline comments only where logic is non-obvious.
|
|
19
|
+
- For smart contracts: always include NatSpec, SPDX license, pragma.
|
|
20
|
+
- For React: use TypeScript, functional components, Tailwind CSS.
|
|
21
|
+
- For MCP tools: follow the Noelclaw pattern (Zod schema, handler returns ToolResult | null).
|
|
22
|
+
- Never include secrets or private keys in output.`;
|
|
23
|
+
async function callLLM(systemPrompt, userPrompt, maxTokens = 4096) {
|
|
24
|
+
const key = process.env.BANKR_API_KEY;
|
|
25
|
+
if (!key)
|
|
26
|
+
throw new Error("BANKR_API_KEY not set — add it to your .env");
|
|
27
|
+
const res = await fetch(BANKR_LLM_URL, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "Content-Type": "application/json", "X-API-Key": key },
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
model: BANKR_MODEL,
|
|
32
|
+
messages: [
|
|
33
|
+
{ role: "system", content: systemPrompt },
|
|
34
|
+
{ role: "user", content: userPrompt },
|
|
35
|
+
],
|
|
36
|
+
max_tokens: maxTokens,
|
|
37
|
+
}),
|
|
38
|
+
signal: AbortSignal.timeout(90000),
|
|
39
|
+
});
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
const body = await res.text().catch(() => "");
|
|
42
|
+
throw new Error(`LLM error ${res.status}: ${body.slice(0, 200)}`);
|
|
43
|
+
}
|
|
44
|
+
const data = await res.json();
|
|
45
|
+
return data.choices?.[0]?.message?.content ?? "";
|
|
46
|
+
}
|
|
47
|
+
function ok(text) {
|
|
48
|
+
return { content: [{ type: "text", text }] };
|
|
49
|
+
}
|
|
50
|
+
function err(text) {
|
|
51
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
52
|
+
}
|
|
53
|
+
// ── Tool definitions ──────────────────────────────────────────────────────────
|
|
54
|
+
exports.CODER_TOOLS = [
|
|
55
|
+
{
|
|
56
|
+
name: "scaffold_project",
|
|
57
|
+
description: "Generate a complete project scaffold for a described idea — file tree + key file contents. " +
|
|
58
|
+
"Supports: Solidity contract projects, MCP skill packages, React/Next.js dApps, Convex backends, CLI tools.",
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
description: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "What to build — be specific: tech stack, purpose, target chain, key features",
|
|
65
|
+
},
|
|
66
|
+
stack: {
|
|
67
|
+
type: "string",
|
|
68
|
+
enum: ["solidity", "mcp-skill", "react-dapp", "convex-backend", "node-cli", "auto"],
|
|
69
|
+
description: "Tech stack. Use 'auto' to let Noel decide based on description.",
|
|
70
|
+
},
|
|
71
|
+
extras: {
|
|
72
|
+
type: "string",
|
|
73
|
+
description: "Optional: extra requirements, constraints, or preferred libraries",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
required: ["description"],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "generate_component",
|
|
81
|
+
description: "Generate a production-ready React/TypeScript component. Returns the full .tsx file content. " +
|
|
82
|
+
"Includes props typing, Tailwind styling, and any hooks needed.",
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
description: {
|
|
87
|
+
type: "string",
|
|
88
|
+
description: "What the component does, what it shows, and how it should behave",
|
|
89
|
+
},
|
|
90
|
+
name: {
|
|
91
|
+
type: "string",
|
|
92
|
+
description: "Component name in PascalCase, e.g. 'TokenPriceCard'",
|
|
93
|
+
},
|
|
94
|
+
context: {
|
|
95
|
+
type: "string",
|
|
96
|
+
description: "Optional: existing imports, store usage, or API calls the component should use",
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
required: ["description", "name"],
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "generate_contract",
|
|
104
|
+
description: "Generate a Solidity smart contract from a description. Returns the full .sol file. " +
|
|
105
|
+
"Follows OpenZeppelin patterns, includes NatSpec, and is Base/EVM compatible.",
|
|
106
|
+
inputSchema: {
|
|
107
|
+
type: "object",
|
|
108
|
+
properties: {
|
|
109
|
+
description: {
|
|
110
|
+
type: "string",
|
|
111
|
+
description: "What the contract does — token type, mechanics, access control, any special logic",
|
|
112
|
+
},
|
|
113
|
+
name: {
|
|
114
|
+
type: "string",
|
|
115
|
+
description: "Contract name in PascalCase, e.g. 'NoelRewards'",
|
|
116
|
+
},
|
|
117
|
+
features: {
|
|
118
|
+
type: "array",
|
|
119
|
+
items: { type: "string" },
|
|
120
|
+
description: "Optional: specific features to include, e.g. ['ERC-20', 'burnable', 'pausable', 'ownable']",
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
required: ["description", "name"],
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "audit_contract",
|
|
128
|
+
description: "Audit a Solidity smart contract for security vulnerabilities, gas inefficiencies, and logic bugs. " +
|
|
129
|
+
"Returns a structured report with severity ratings and fix recommendations.",
|
|
130
|
+
inputSchema: {
|
|
131
|
+
type: "object",
|
|
132
|
+
properties: {
|
|
133
|
+
code: {
|
|
134
|
+
type: "string",
|
|
135
|
+
description: "The full Solidity contract source code to audit",
|
|
136
|
+
},
|
|
137
|
+
focus: {
|
|
138
|
+
type: "array",
|
|
139
|
+
items: { type: "string" },
|
|
140
|
+
description: "Optional: specific areas to focus on, e.g. ['reentrancy', 'access control', 'overflow']",
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
required: ["code"],
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "explain_code",
|
|
148
|
+
description: "Explain what a piece of code does in plain language. Works with Solidity, TypeScript, JavaScript, Python, and more. " +
|
|
149
|
+
"Breaks down logic, highlights key patterns, and explains the 'why' behind decisions.",
|
|
150
|
+
inputSchema: {
|
|
151
|
+
type: "object",
|
|
152
|
+
properties: {
|
|
153
|
+
code: {
|
|
154
|
+
type: "string",
|
|
155
|
+
description: "The code to explain",
|
|
156
|
+
},
|
|
157
|
+
depth: {
|
|
158
|
+
type: "string",
|
|
159
|
+
enum: ["overview", "detailed", "line-by-line"],
|
|
160
|
+
description: "How detailed the explanation should be. Default: 'detailed'",
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
required: ["code"],
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: "review_code",
|
|
168
|
+
description: "Review and improve a piece of code. Returns the improved version with a summary of changes. " +
|
|
169
|
+
"Fixes bugs, improves readability, adds types, optimizes gas (for Solidity), and removes anti-patterns.",
|
|
170
|
+
inputSchema: {
|
|
171
|
+
type: "object",
|
|
172
|
+
properties: {
|
|
173
|
+
code: {
|
|
174
|
+
type: "string",
|
|
175
|
+
description: "The code to review and improve",
|
|
176
|
+
},
|
|
177
|
+
language: {
|
|
178
|
+
type: "string",
|
|
179
|
+
description: "Language/framework, e.g. 'solidity', 'typescript', 'react', 'python'. Auto-detected if omitted.",
|
|
180
|
+
},
|
|
181
|
+
goals: {
|
|
182
|
+
type: "string",
|
|
183
|
+
description: "Optional: specific improvement goals, e.g. 'reduce gas costs', 'improve readability', 'add error handling'",
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
required: ["code"],
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
// ── Handlers ──────────────────────────────────────────────────────────────────
|
|
191
|
+
const ScaffoldSchema = zod_1.z.object({
|
|
192
|
+
description: zod_1.z.string().min(10),
|
|
193
|
+
stack: zod_1.z.enum(["solidity", "mcp-skill", "react-dapp", "convex-backend", "node-cli", "auto"]).optional(),
|
|
194
|
+
extras: zod_1.z.string().optional(),
|
|
195
|
+
});
|
|
196
|
+
const ComponentSchema = zod_1.z.object({
|
|
197
|
+
description: zod_1.z.string().min(5),
|
|
198
|
+
name: zod_1.z.string().min(1),
|
|
199
|
+
context: zod_1.z.string().optional(),
|
|
200
|
+
});
|
|
201
|
+
const ContractSchema = zod_1.z.object({
|
|
202
|
+
description: zod_1.z.string().min(5),
|
|
203
|
+
name: zod_1.z.string().min(1),
|
|
204
|
+
features: zod_1.z.array(zod_1.z.string()).optional(),
|
|
205
|
+
});
|
|
206
|
+
const AuditSchema = zod_1.z.object({
|
|
207
|
+
code: zod_1.z.string().min(10),
|
|
208
|
+
focus: zod_1.z.array(zod_1.z.string()).optional(),
|
|
209
|
+
});
|
|
210
|
+
const ExplainSchema = zod_1.z.object({
|
|
211
|
+
code: zod_1.z.string().min(1),
|
|
212
|
+
depth: zod_1.z.enum(["overview", "detailed", "line-by-line"]).optional(),
|
|
213
|
+
});
|
|
214
|
+
const ReviewSchema = zod_1.z.object({
|
|
215
|
+
code: zod_1.z.string().min(1),
|
|
216
|
+
language: zod_1.z.string().optional(),
|
|
217
|
+
goals: zod_1.z.string().optional(),
|
|
218
|
+
});
|
|
219
|
+
async function handleCoderTool(name, args) {
|
|
220
|
+
switch (name) {
|
|
221
|
+
case "scaffold_project": {
|
|
222
|
+
const p = ScaffoldSchema.safeParse(args);
|
|
223
|
+
if (!p.success)
|
|
224
|
+
return err(`Invalid input: ${p.error.message}`);
|
|
225
|
+
const { description, stack = "auto", extras } = p.data;
|
|
226
|
+
const prompt = `Generate a complete project scaffold for the following:\n\n` +
|
|
227
|
+
`Description: ${description}\n` +
|
|
228
|
+
`Stack: ${stack}\n` +
|
|
229
|
+
(extras ? `Extra requirements: ${extras}\n` : "") +
|
|
230
|
+
`\nOutput format:\n` +
|
|
231
|
+
`1. Brief overview (2-3 sentences)\n` +
|
|
232
|
+
`2. Full file tree with all files listed\n` +
|
|
233
|
+
`3. Full content of each key file (package.json, main entry, config, one core module)\n` +
|
|
234
|
+
`4. Setup instructions (3-5 steps)\n\n` +
|
|
235
|
+
`Use real, runnable code. No TODO placeholders.`;
|
|
236
|
+
try {
|
|
237
|
+
const response = await callLLM(CODER_SYSTEM, prompt, 4096);
|
|
238
|
+
return ok(response);
|
|
239
|
+
}
|
|
240
|
+
catch (e) {
|
|
241
|
+
return err(`scaffold_project failed: ${e.message}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
case "generate_component": {
|
|
245
|
+
const p = ComponentSchema.safeParse(args);
|
|
246
|
+
if (!p.success)
|
|
247
|
+
return err(`Invalid input: ${p.error.message}`);
|
|
248
|
+
const { description, name: componentName, context } = p.data;
|
|
249
|
+
const prompt = `Generate a production-ready React TypeScript component.\n\n` +
|
|
250
|
+
`Component name: ${componentName}\n` +
|
|
251
|
+
`Description: ${description}\n` +
|
|
252
|
+
(context ? `Context / existing code to integrate with:\n${context}\n` : "") +
|
|
253
|
+
`\nRequirements:\n` +
|
|
254
|
+
`- Full .tsx file, no omissions\n` +
|
|
255
|
+
`- TypeScript props interface\n` +
|
|
256
|
+
`- Tailwind CSS for styling\n` +
|
|
257
|
+
`- Use lucide-react for icons if needed\n` +
|
|
258
|
+
`- Framer Motion for animations if the component is interactive\n` +
|
|
259
|
+
`- Named export (not default)\n` +
|
|
260
|
+
`Output only the .tsx file content, no prose before or after.`;
|
|
261
|
+
try {
|
|
262
|
+
const response = await callLLM(CODER_SYSTEM, prompt, 3000);
|
|
263
|
+
return ok(response);
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
return err(`generate_component failed: ${e.message}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
case "generate_contract": {
|
|
270
|
+
const p = ContractSchema.safeParse(args);
|
|
271
|
+
if (!p.success)
|
|
272
|
+
return err(`Invalid input: ${p.error.message}`);
|
|
273
|
+
const { description, name: contractName, features = [] } = p.data;
|
|
274
|
+
const prompt = `Generate a production-ready Solidity smart contract.\n\n` +
|
|
275
|
+
`Contract name: ${contractName}\n` +
|
|
276
|
+
`Description: ${description}\n` +
|
|
277
|
+
(features.length ? `Required features: ${features.join(", ")}\n` : "") +
|
|
278
|
+
`\nRequirements:\n` +
|
|
279
|
+
`- Solidity ^0.8.20\n` +
|
|
280
|
+
`- SPDX-License-Identifier: MIT\n` +
|
|
281
|
+
`- Use OpenZeppelin contracts where appropriate (import paths: @openzeppelin/contracts/...)\n` +
|
|
282
|
+
`- Full NatSpec comments on all public functions\n` +
|
|
283
|
+
`- Optimized for Base chain (EVM compatible)\n` +
|
|
284
|
+
`- Include events for all state changes\n` +
|
|
285
|
+
`- Include a basic test outline as a comment at the bottom\n` +
|
|
286
|
+
`Output only the .sol file content.`;
|
|
287
|
+
try {
|
|
288
|
+
const response = await callLLM(CODER_SYSTEM, prompt, 3500);
|
|
289
|
+
return ok(response);
|
|
290
|
+
}
|
|
291
|
+
catch (e) {
|
|
292
|
+
return err(`generate_contract failed: ${e.message}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
case "audit_contract": {
|
|
296
|
+
const p = AuditSchema.safeParse(args);
|
|
297
|
+
if (!p.success)
|
|
298
|
+
return err(`Invalid input: ${p.error.message}`);
|
|
299
|
+
const { code, focus = [] } = p.data;
|
|
300
|
+
const prompt = `Audit the following Solidity smart contract for security vulnerabilities, gas issues, and logic bugs.\n` +
|
|
301
|
+
(focus.length ? `Focus areas: ${focus.join(", ")}\n` : "") +
|
|
302
|
+
`\nContract:\n\`\`\`solidity\n${code}\n\`\`\`\n\n` +
|
|
303
|
+
`Output a structured audit report with these sections:\n` +
|
|
304
|
+
`## Summary\n` +
|
|
305
|
+
`(overall risk rating: Critical/High/Medium/Low/Informational, brief overview)\n\n` +
|
|
306
|
+
`## Findings\n` +
|
|
307
|
+
`For each issue:\n` +
|
|
308
|
+
`- **[SEVERITY]** Title\n` +
|
|
309
|
+
` - Location: (function/line)\n` +
|
|
310
|
+
` - Description: (what's wrong and why)\n` +
|
|
311
|
+
` - Recommendation: (exact fix or mitigation)\n\n` +
|
|
312
|
+
`## Gas Optimizations\n` +
|
|
313
|
+
`(list 2-5 concrete gas savings with estimated impact)\n\n` +
|
|
314
|
+
`## Positive Patterns\n` +
|
|
315
|
+
`(what the contract does well)`;
|
|
316
|
+
try {
|
|
317
|
+
const response = await callLLM(CODER_SYSTEM, prompt, 3000);
|
|
318
|
+
return ok(response);
|
|
319
|
+
}
|
|
320
|
+
catch (e) {
|
|
321
|
+
return err(`audit_contract failed: ${e.message}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
case "explain_code": {
|
|
325
|
+
const p = ExplainSchema.safeParse(args);
|
|
326
|
+
if (!p.success)
|
|
327
|
+
return err(`Invalid input: ${p.error.message}`);
|
|
328
|
+
const { code, depth = "detailed" } = p.data;
|
|
329
|
+
const depthInstructions = {
|
|
330
|
+
overview: "Give a high-level explanation in 3-5 sentences. What does it do, what problem does it solve, what are the main moving parts.",
|
|
331
|
+
detailed: "Explain the purpose, break down each major section, explain key patterns and design decisions, and note any non-obvious logic.",
|
|
332
|
+
"line-by-line": "Go through the code line by line (or block by block for repetitive parts), explaining exactly what each part does and why.",
|
|
333
|
+
};
|
|
334
|
+
const prompt = `Explain the following code.\n\nDepth: ${depth}\n${depthInstructions[depth]}\n\n` +
|
|
335
|
+
`Code:\n\`\`\`\n${code}\n\`\`\`\n\n` +
|
|
336
|
+
`Write in plain language. Assume the reader understands programming but may not know this specific codebase or language.`;
|
|
337
|
+
try {
|
|
338
|
+
const response = await callLLM(CODER_SYSTEM, prompt, 2000);
|
|
339
|
+
return ok(response);
|
|
340
|
+
}
|
|
341
|
+
catch (e) {
|
|
342
|
+
return err(`explain_code failed: ${e.message}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
case "review_code": {
|
|
346
|
+
const p = ReviewSchema.safeParse(args);
|
|
347
|
+
if (!p.success)
|
|
348
|
+
return err(`Invalid input: ${p.error.message}`);
|
|
349
|
+
const { code, language, goals } = p.data;
|
|
350
|
+
const prompt = `Review and improve the following code.\n` +
|
|
351
|
+
(language ? `Language/framework: ${language}\n` : "") +
|
|
352
|
+
(goals ? `Improvement goals: ${goals}\n` : "") +
|
|
353
|
+
`\nOriginal code:\n\`\`\`\n${code}\n\`\`\`\n\n` +
|
|
354
|
+
`Output:\n` +
|
|
355
|
+
`## What I changed\n` +
|
|
356
|
+
`(bullet list: each change and why)\n\n` +
|
|
357
|
+
`## Improved code\n` +
|
|
358
|
+
`\`\`\`\n(full improved code — no omissions, no "rest of code unchanged")\n\`\`\`\n\n` +
|
|
359
|
+
`## Further recommendations\n` +
|
|
360
|
+
`(optional: things to consider beyond this snippet)`;
|
|
361
|
+
try {
|
|
362
|
+
const response = await callLLM(CODER_SYSTEM, prompt, 3500);
|
|
363
|
+
return ok(response);
|
|
364
|
+
}
|
|
365
|
+
catch (e) {
|
|
366
|
+
return err(`review_code failed: ${e.message}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
default:
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
}
|
package/dist/tools/defi.js
CHANGED
|
@@ -6,6 +6,24 @@ const zod_1 = require("zod");
|
|
|
6
6
|
const convex_js_1 = require("../convex.js");
|
|
7
7
|
const wallet_js_1 = require("../wallet.js");
|
|
8
8
|
exports.DEFI_TOOLS = [
|
|
9
|
+
{
|
|
10
|
+
name: "get_portfolio",
|
|
11
|
+
description: "Get current token balances and total portfolio value for your MCP wallet on Base. Always call this before swapping to confirm available balance.",
|
|
12
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: "estimate_swap",
|
|
16
|
+
description: "Preview a swap — get the expected output amount and price impact without executing. Use this before swap_tokens to confirm the rate is acceptable.",
|
|
17
|
+
inputSchema: {
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
fromToken: { type: "string", description: "Token to sell: ETH, USDC, USDT, DAI, WETH" },
|
|
21
|
+
toToken: { type: "string", description: "Token to buy: ETH, USDC, USDT, DAI, WETH" },
|
|
22
|
+
amount: { type: "string", description: "Amount to swap (e.g. '0.01', '50', '100%')" },
|
|
23
|
+
},
|
|
24
|
+
required: ["fromToken", "toToken", "amount"],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
9
27
|
{
|
|
10
28
|
name: "swap_tokens",
|
|
11
29
|
description: "Swap tokens on Base mainnet via 0x Permit2. Supported: ETH, USDC, USDT, DAI, WETH. Amount is human-readable. Signed and broadcast locally from your wallet.",
|
|
@@ -32,11 +50,65 @@ exports.DEFI_TOOLS = [
|
|
|
32
50
|
required: ["token", "toAddress", "amount"],
|
|
33
51
|
},
|
|
34
52
|
},
|
|
53
|
+
{
|
|
54
|
+
name: "scan_wallet",
|
|
55
|
+
description: "AI-powered portfolio scan — concentration risk, volatility exposure, Base ecosystem opportunities, and a concrete 3-step action plan based on your actual holdings. Requires wallet auth.",
|
|
56
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
57
|
+
},
|
|
35
58
|
];
|
|
36
59
|
const SwapSchema = zod_1.z.object({ fromToken: zod_1.z.string().min(1), toToken: zod_1.z.string().min(1), amount: zod_1.z.string().min(1) });
|
|
37
60
|
const SendSchema = zod_1.z.object({ token: zod_1.z.string().min(1), toAddress: zod_1.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "must be a valid 0x address"), amount: zod_1.z.string().min(1) });
|
|
61
|
+
const BUY_DECIMALS = { USDC: 6, USDT: 6, DAI: 18, ETH: 18, WETH: 18 };
|
|
62
|
+
function formatTokenAmount(raw, token) {
|
|
63
|
+
const dec = BUY_DECIMALS[token.toUpperCase()] ?? 18;
|
|
64
|
+
return (parseInt(raw) / Math.pow(10, dec)).toFixed(dec === 6 ? 2 : 6);
|
|
65
|
+
}
|
|
38
66
|
async function handleDefiTool(name, args) {
|
|
39
67
|
switch (name) {
|
|
68
|
+
case "get_portfolio": {
|
|
69
|
+
const result = await (0, convex_js_1.callConvex)("/mcp/defi/portfolio", "GET", undefined, "get_portfolio");
|
|
70
|
+
if (result.error)
|
|
71
|
+
return { content: [{ type: "text", text: `Portfolio fetch failed: ${result.error}` }], isError: true };
|
|
72
|
+
const balances = result.balances ?? [];
|
|
73
|
+
const totalUsd = result.totalUsd ?? balances.reduce((s, b) => s + (b.valueUsd ?? 0), 0);
|
|
74
|
+
if (!balances.length) {
|
|
75
|
+
return { content: [{ type: "text", text: "Your wallet has no tokens yet. Send ETH or USDC on Base to get started." }] };
|
|
76
|
+
}
|
|
77
|
+
const lines = [`**Portfolio** — Total: $${totalUsd.toFixed(2)}`, ""];
|
|
78
|
+
for (const b of balances) {
|
|
79
|
+
const value = b.valueUsd != null ? ` ($${Number(b.valueUsd).toFixed(2)})` : "";
|
|
80
|
+
lines.push(`• **${b.token ?? b.symbol}**: ${Number(b.balance ?? b.amount).toLocaleString(undefined, { maximumFractionDigits: 6 })}${value}`);
|
|
81
|
+
}
|
|
82
|
+
lines.push("", `Wallet: \`${result.address ?? "unknown"}\``);
|
|
83
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
84
|
+
}
|
|
85
|
+
case "estimate_swap": {
|
|
86
|
+
const parsed = SwapSchema.safeParse(args);
|
|
87
|
+
if (!parsed.success)
|
|
88
|
+
return { content: [{ type: "text", text: `Invalid input: ${String(parsed.error.issues[0].path[0])} ${parsed.error.issues[0].message}` }], isError: true };
|
|
89
|
+
const { fromToken, toToken, amount } = parsed.data;
|
|
90
|
+
const result = await (0, convex_js_1.callConvex)("/mcp/defi/swap", "POST", { fromToken, toToken, amount }, "estimate_swap");
|
|
91
|
+
if (!result.success)
|
|
92
|
+
return { content: [{ type: "text", text: `Estimate failed: ${result.error}` }], isError: true };
|
|
93
|
+
const q = result.quote;
|
|
94
|
+
const buyHuman = formatTokenAmount(q.buyAmount, q.buyToken ?? toToken);
|
|
95
|
+
const sellHuman = formatTokenAmount(q.sellAmount ?? "0", q.sellToken ?? fromToken);
|
|
96
|
+
const priceImpact = q.estimatedPriceImpact != null ? `${Number(q.estimatedPriceImpact).toFixed(3)}%` : "< 0.01%";
|
|
97
|
+
return {
|
|
98
|
+
content: [{
|
|
99
|
+
type: "text",
|
|
100
|
+
text: [
|
|
101
|
+
`**Swap Estimate** (not executed)`,
|
|
102
|
+
``,
|
|
103
|
+
`You sell: **${sellHuman} ${(q.sellToken ?? fromToken).toUpperCase()}**`,
|
|
104
|
+
`You get: **~${buyHuman} ${(q.buyToken ?? toToken).toUpperCase()}**`,
|
|
105
|
+
`Price impact: ${priceImpact}`,
|
|
106
|
+
``,
|
|
107
|
+
`Run \`swap_tokens\` with the same params to execute.`,
|
|
108
|
+
].join("\n"),
|
|
109
|
+
}],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
40
112
|
case "swap_tokens": {
|
|
41
113
|
const parsed = SwapSchema.safeParse(args);
|
|
42
114
|
if (!parsed.success)
|
|
@@ -47,7 +119,7 @@ async function handleDefiTool(name, args) {
|
|
|
47
119
|
if (!result.success)
|
|
48
120
|
return { content: [{ type: "text", text: `Swap failed: ${result.error}` }], isError: true };
|
|
49
121
|
const txHash = await (0, wallet_js_1.signAndBroadcast)(wallet, result.quote);
|
|
50
|
-
const buyAmountHuman = (
|
|
122
|
+
const buyAmountHuman = formatTokenAmount(result.quote.buyAmount, result.quote.buyToken ?? toToken);
|
|
51
123
|
return {
|
|
52
124
|
content: [{
|
|
53
125
|
type: "text",
|
|
@@ -78,6 +150,39 @@ async function handleDefiTool(name, args) {
|
|
|
78
150
|
}],
|
|
79
151
|
};
|
|
80
152
|
}
|
|
153
|
+
case "scan_wallet": {
|
|
154
|
+
const data = await (0, convex_js_1.callConvex)("/wallet/scan", "GET", undefined, "scan_wallet");
|
|
155
|
+
if (data.error) {
|
|
156
|
+
return { content: [{ type: "text", text: `Scan failed: ${data.error}` }], isError: true };
|
|
157
|
+
}
|
|
158
|
+
const total = (data.totalUsd ?? 0).toFixed(2);
|
|
159
|
+
const topHoldings = (data.holdings ?? [])
|
|
160
|
+
.slice(0, 5)
|
|
161
|
+
.map((h) => `• **${h.token}**: $${(h.valueUsd ?? 0).toFixed(2)}${h.pct != null ? ` (${h.pct}%)` : ""}`)
|
|
162
|
+
.join("\n");
|
|
163
|
+
const header = [
|
|
164
|
+
`**Portfolio Scan** — Total: $${total}`,
|
|
165
|
+
`Wallet: \`${data.address ?? "unknown"}\``,
|
|
166
|
+
``,
|
|
167
|
+
`**Holdings:**`,
|
|
168
|
+
topHoldings,
|
|
169
|
+
``,
|
|
170
|
+
].join("\n");
|
|
171
|
+
let body;
|
|
172
|
+
if (data.analysis) {
|
|
173
|
+
body = data.analysis;
|
|
174
|
+
}
|
|
175
|
+
else if (data.analysisError) {
|
|
176
|
+
body = `*AI analysis unavailable: ${data.analysisError}*`;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
body = "*AI analysis not available*";
|
|
180
|
+
}
|
|
181
|
+
const footer = data.tokensUsed
|
|
182
|
+
? `\n\n*Tokens used: ${data.tokensUsed} · Scanned: ${data.scannedAt ?? ""}*`
|
|
183
|
+
: "";
|
|
184
|
+
return { content: [{ type: "text", text: header + body + footer }] };
|
|
185
|
+
}
|
|
81
186
|
default:
|
|
82
187
|
return null;
|
|
83
188
|
}
|
package/dist/tools/humanizer.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.HUMANIZER_TOOLS = void 0;
|
|
4
4
|
exports.handleHumanizerTool = handleHumanizerTool;
|
|
5
|
+
const zod_1 = require("zod");
|
|
5
6
|
exports.HUMANIZER_TOOLS = [
|
|
6
7
|
{
|
|
7
8
|
name: "humanize_text",
|
|
@@ -25,6 +26,12 @@ exports.HUMANIZER_TOOLS = [
|
|
|
25
26
|
},
|
|
26
27
|
},
|
|
27
28
|
];
|
|
29
|
+
const HumanizerSchema = zod_1.z.object({
|
|
30
|
+
text: zod_1.z.string().min(1),
|
|
31
|
+
voice_sample: zod_1.z.string().optional(),
|
|
32
|
+
});
|
|
33
|
+
const BANKR_LLM_URL = "https://llm.bankr.bot/v1/chat/completions";
|
|
34
|
+
const BANKR_MODEL = process.env.BANKR_MODEL ?? "grok-3";
|
|
28
35
|
const HUMANIZER_SYSTEM = `You are a text editor that removes signs of AI-generated writing.
|
|
29
36
|
|
|
30
37
|
Your job: rewrite the input so it sounds natural, direct, and human — without changing the meaning.
|
|
@@ -75,26 +82,27 @@ If a voice sample is provided, match its tone, rhythm, and vocabulary. Otherwise
|
|
|
75
82
|
async function handleHumanizerTool(name, args) {
|
|
76
83
|
if (name !== "humanize_text")
|
|
77
84
|
return null;
|
|
78
|
-
const
|
|
79
|
-
if (!
|
|
80
|
-
return { content: [{ type: "text", text:
|
|
85
|
+
const parsed = HumanizerSchema.safeParse(args);
|
|
86
|
+
if (!parsed.success) {
|
|
87
|
+
return { content: [{ type: "text", text: `Invalid input: text ${parsed.error.issues[0].message}` }], isError: true };
|
|
81
88
|
}
|
|
82
|
-
const
|
|
89
|
+
const { text, voice_sample } = parsed.data;
|
|
90
|
+
const apiKey = process.env.BANKR_API_KEY;
|
|
83
91
|
if (!apiKey) {
|
|
84
|
-
return { content: [{ type: "text", text: "
|
|
92
|
+
return { content: [{ type: "text", text: "BANKR_API_KEY not set — add it to your MCP env config." }], isError: true };
|
|
85
93
|
}
|
|
86
|
-
const userMsg =
|
|
87
|
-
? `VOICE SAMPLE (match this style):\n${
|
|
88
|
-
:
|
|
94
|
+
const userMsg = voice_sample
|
|
95
|
+
? `VOICE SAMPLE (match this style):\n${voice_sample}\n\n---\n\nTEXT TO HUMANIZE:\n${text}`
|
|
96
|
+
: text;
|
|
89
97
|
try {
|
|
90
|
-
const res = await fetch(
|
|
98
|
+
const res = await fetch(BANKR_LLM_URL, {
|
|
91
99
|
method: "POST",
|
|
92
100
|
headers: {
|
|
101
|
+
"X-API-Key": apiKey,
|
|
93
102
|
"Content-Type": "application/json",
|
|
94
|
-
"Authorization": `Bearer ${apiKey}`,
|
|
95
103
|
},
|
|
96
104
|
body: JSON.stringify({
|
|
97
|
-
model:
|
|
105
|
+
model: BANKR_MODEL,
|
|
98
106
|
messages: [
|
|
99
107
|
{ role: "system", content: HUMANIZER_SYSTEM },
|
|
100
108
|
{ role: "user", content: userMsg },
|
|
@@ -105,12 +113,11 @@ async function handleHumanizerTool(name, args) {
|
|
|
105
113
|
signal: AbortSignal.timeout(30000),
|
|
106
114
|
});
|
|
107
115
|
if (!res.ok) {
|
|
108
|
-
const err = await res.text();
|
|
109
|
-
return { content: [{ type: "text", text: `
|
|
116
|
+
const err = await res.text().catch(() => res.statusText);
|
|
117
|
+
return { content: [{ type: "text", text: `Bankr LLM error ${res.status}: ${err.slice(0, 200)}` }], isError: true };
|
|
110
118
|
}
|
|
111
119
|
const data = await res.json();
|
|
112
|
-
const
|
|
113
|
-
const output = raw.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
|
|
120
|
+
const output = data.choices?.[0]?.message?.content?.trim() ?? "";
|
|
114
121
|
if (!output)
|
|
115
122
|
return { content: [{ type: "text", text: "Empty response from model" }], isError: true };
|
|
116
123
|
return { content: [{ type: "text", text: output }] };
|
package/dist/tools/insight.js
CHANGED
|
@@ -30,16 +30,57 @@ const AskNoelSchema = zod_1.z.object({
|
|
|
30
30
|
question: zod_1.z.string().min(1),
|
|
31
31
|
messages: zod_1.z.array(zod_1.z.object({ role: zod_1.z.enum(["user", "assistant"]), content: zod_1.z.string() })).optional(),
|
|
32
32
|
});
|
|
33
|
+
const BANKR_LLM_URL = "https://llm.bankr.bot/v1/chat/completions";
|
|
34
|
+
const BANKR_MODEL = process.env.BANKR_MODEL ?? "grok-3";
|
|
35
|
+
const NOEL_SYSTEM_PROMPT = `You are Noel, a crypto AI analyst with deep expertise in DeFi, on-chain data, market structure, and trading psychology. You provide sharp, direct analysis — no fluff, no disclaimers. You understand narratives, liquidity flows, whale behavior, and how sentiment drives price. When asked about a token or market, give your honest read with supporting reasoning.`;
|
|
36
|
+
async function askViaBankr(question, messages) {
|
|
37
|
+
const res = await fetch(BANKR_LLM_URL, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: {
|
|
40
|
+
"X-API-Key": process.env.BANKR_API_KEY,
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
},
|
|
43
|
+
body: JSON.stringify({
|
|
44
|
+
model: BANKR_MODEL,
|
|
45
|
+
messages: [
|
|
46
|
+
{ role: "system", content: NOEL_SYSTEM_PROMPT },
|
|
47
|
+
...messages,
|
|
48
|
+
{ role: "user", content: question },
|
|
49
|
+
],
|
|
50
|
+
max_tokens: 1024,
|
|
51
|
+
}),
|
|
52
|
+
signal: AbortSignal.timeout(30000),
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
const err = await res.text().catch(() => res.statusText);
|
|
56
|
+
throw new Error(`Bankr LLM error ${res.status}: ${err.slice(0, 200)}`);
|
|
57
|
+
}
|
|
58
|
+
const data = await res.json();
|
|
59
|
+
return data.choices?.[0]?.message?.content ?? "No response from model";
|
|
60
|
+
}
|
|
33
61
|
async function handleInsightTool(name, args) {
|
|
34
62
|
if (name !== "ask_noel")
|
|
35
63
|
return null;
|
|
36
64
|
const parsed = AskNoelSchema.safeParse(args);
|
|
37
65
|
if (!parsed.success)
|
|
38
66
|
return { content: [{ type: "text", text: `Invalid input: question ${parsed.error.issues[0].message}` }], isError: true };
|
|
67
|
+
const { question, messages = [] } = parsed.data;
|
|
68
|
+
// If BANKR_API_KEY is set, call Bankr LLM directly — faster, no Convex hop
|
|
69
|
+
if (process.env.BANKR_API_KEY) {
|
|
70
|
+
try {
|
|
71
|
+
const answer = await askViaBankr(question, messages);
|
|
72
|
+
return { content: [{ type: "text", text: answer }] };
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
// Fall through to Convex if Bankr call fails
|
|
76
|
+
console.error(`Bankr LLM failed, falling back to Convex: ${err.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Fallback: route through Convex backend
|
|
39
80
|
const data = await (0, convex_js_1.callConvex)("/mcp/chat", "POST", {
|
|
40
|
-
question
|
|
81
|
+
question,
|
|
41
82
|
agentId: "noel-default",
|
|
42
|
-
messages
|
|
83
|
+
messages,
|
|
43
84
|
}, "ask_noel");
|
|
44
85
|
return { content: [{ type: "text", text: data.answer ?? JSON.stringify(data) }] };
|
|
45
86
|
}
|