@noelclaw/mcp 1.5.7 → 2.2.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.
@@ -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 = (parseInt(result.quote.buyAmount) / 1e6).toFixed(4);
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
  }
@@ -2,6 +2,8 @@
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");
6
+ const llm_js_1 = require("../llm.js");
5
7
  exports.HUMANIZER_TOOLS = [
6
8
  {
7
9
  name: "humanize_text",
@@ -25,6 +27,10 @@ exports.HUMANIZER_TOOLS = [
25
27
  },
26
28
  },
27
29
  ];
30
+ const HumanizerSchema = zod_1.z.object({
31
+ text: zod_1.z.string().min(1),
32
+ voice_sample: zod_1.z.string().optional(),
33
+ });
28
34
  const HUMANIZER_SYSTEM = `You are a text editor that removes signs of AI-generated writing.
29
35
 
30
36
  Your job: rewrite the input so it sounds natural, direct, and human — without changing the meaning.
@@ -75,45 +81,19 @@ If a voice sample is provided, match its tone, rhythm, and vocabulary. Otherwise
75
81
  async function handleHumanizerTool(name, args) {
76
82
  if (name !== "humanize_text")
77
83
  return null;
78
- const a = (args ?? {});
79
- if (!a.text?.trim()) {
80
- return { content: [{ type: "text", text: "text is required" }], isError: true };
84
+ const parsed = HumanizerSchema.safeParse(args);
85
+ if (!parsed.success) {
86
+ return { content: [{ type: "text", text: `Invalid input: text ${parsed.error.issues[0].message}` }], isError: true };
81
87
  }
82
- const apiKey = process.env.MINIMAX_API_KEY;
83
- if (!apiKey) {
84
- return { content: [{ type: "text", text: "MINIMAX_API_KEY not set — humanizer requires MiniMax API access" }], isError: true };
85
- }
86
- const userMsg = a.voice_sample
87
- ? `VOICE SAMPLE (match this style):\n${a.voice_sample}\n\n---\n\nTEXT TO HUMANIZE:\n${a.text}`
88
- : a.text;
88
+ const { text, voice_sample } = parsed.data;
89
+ const userMsg = voice_sample
90
+ ? `VOICE SAMPLE (match this style):\n${voice_sample}\n\n---\n\nTEXT TO HUMANIZE:\n${text}`
91
+ : text;
89
92
  try {
90
- const res = await fetch("https://api.minimaxi.chat/v1/chat/completions", {
91
- method: "POST",
92
- headers: {
93
- "Content-Type": "application/json",
94
- "Authorization": `Bearer ${apiKey}`,
95
- },
96
- body: JSON.stringify({
97
- model: "MiniMax-M2.7",
98
- messages: [
99
- { role: "system", content: HUMANIZER_SYSTEM },
100
- { role: "user", content: userMsg },
101
- ],
102
- temperature: 0.7,
103
- max_tokens: 4096,
104
- }),
105
- signal: AbortSignal.timeout(30000),
106
- });
107
- if (!res.ok) {
108
- const err = await res.text();
109
- return { content: [{ type: "text", text: `API error: ${res.status} ${err.slice(0, 200)}` }], isError: true };
110
- }
111
- const data = await res.json();
112
- const raw = data?.choices?.[0]?.message?.content ?? "";
113
- const output = raw.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
93
+ const output = await (0, llm_js_1.callLLM)(HUMANIZER_SYSTEM, userMsg, 4096);
114
94
  if (!output)
115
95
  return { content: [{ type: "text", text: "Empty response from model" }], isError: true };
116
- return { content: [{ type: "text", text: output }] };
96
+ return { content: [{ type: "text", text: output.trim() }] };
117
97
  }
118
98
  catch (err) {
119
99
  return { content: [{ type: "text", text: `Humanizer error: ${err.message}` }], isError: true };
@@ -4,6 +4,7 @@ exports.INSIGHT_TOOLS = void 0;
4
4
  exports.handleInsightTool = handleInsightTool;
5
5
  const zod_1 = require("zod");
6
6
  const convex_js_1 = require("../convex.js");
7
+ const llm_js_1 = require("../llm.js");
7
8
  exports.INSIGHT_TOOLS = [
8
9
  {
9
10
  name: "ask_noel",
@@ -30,34 +31,7 @@ const AskNoelSchema = zod_1.z.object({
30
31
  question: zod_1.z.string().min(1),
31
32
  messages: zod_1.z.array(zod_1.z.object({ role: zod_1.z.enum(["user", "assistant"]), content: zod_1.z.string() })).optional(),
32
33
  });
33
- const BANKR_LLM_URL = "https://llm.bankr.bot/v1/chat/completions";
34
- const BANKR_MODEL = process.env.BANKR_MODEL ?? "grok-3";
35
34
  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
- }
61
35
  async function handleInsightTool(name, args) {
62
36
  if (name !== "ask_noel")
63
37
  return null;
@@ -65,18 +39,18 @@ async function handleInsightTool(name, args) {
65
39
  if (!parsed.success)
66
40
  return { content: [{ type: "text", text: `Invalid input: question ${parsed.error.issues[0].message}` }], isError: true };
67
41
  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) {
42
+ // Try local LLM first (Anthropic from Claude Desktop, or Bankr if configured)
43
+ if (process.env.ANTHROPIC_API_KEY || process.env.BANKR_API_KEY) {
70
44
  try {
71
- const answer = await askViaBankr(question, messages);
45
+ const history = messages.map(m => ({ role: m.role, content: m.content }));
46
+ const answer = await (0, llm_js_1.callLLM)(NOEL_SYSTEM_PROMPT, question, 1024, history);
72
47
  return { content: [{ type: "text", text: answer }] };
73
48
  }
74
49
  catch (err) {
75
- // Fall through to Convex if Bankr call fails
76
- console.error(`Bankr LLM failed, falling back to Convex: ${err.message}`);
50
+ console.error(`Local LLM failed, falling back to Convex: ${err.message}`);
77
51
  }
78
52
  }
79
- // Fallback: route through Convex backend
53
+ // Fallback: route through Convex backend (uses server's configured key)
80
54
  const data = await (0, convex_js_1.callConvex)("/mcp/chat", "POST", {
81
55
  question,
82
56
  agentId: "noel-default",
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MIROSHARK_TOOLS = void 0;
4
4
  exports.handleMirosharkTool = handleMirosharkTool;
5
+ const llm_js_1 = require("../llm.js");
6
+ const convex_js_1 = require("../convex.js");
5
7
  const CONVEX_SITE = process.env.NOELCLAW_CONVEX_URL ?? "https://api.noelclaw.com";
6
8
  exports.MIROSHARK_TOOLS = [
7
9
  {
@@ -239,25 +241,67 @@ async function handleMirosharkTool(name, args) {
239
241
  };
240
242
  }
241
243
  if (runnerStatus === "completed" || runnerStatus === "stopped") {
242
- // Fetch a sample of agent actions for the summary
243
- const actionsData = await miroJson(`/miroshark/api/simulation/${simId}/actions?limit=10`, "GET").catch(() => ({ actions: [] }));
244
+ const actionsData = await miroJson(`/miroshark/api/simulation/${simId}/actions?limit=50`, "GET").catch(() => ({ actions: [] }));
244
245
  const actions = actionsData?.actions ?? [];
245
- const lines = [
246
- `**MiroShark \`${simId}\`** ${runnerStatus}`,
247
- ``,
248
- `Rounds completed: ${runStatus.current_round ?? "?"}`,
249
- `Total actions: ${runStatus.total_actions_count ?? actions.length}`,
250
- ];
251
- if (actions.length > 0) {
252
- lines.push("", "**Sample agent activity:**");
253
- for (const act of actions.slice(0, 8)) {
246
+ const totalActions = runStatus.total_actions_count ?? actions.length;
247
+ const rounds = runStatus.current_round ?? "?";
248
+ const ACTION_EMOJI = {
249
+ tweet: "🐦", post: "📝", sell: "📉", buy: "📈",
250
+ article: "📰", comment: "💬", alert: "🚨", analyze: "🔍",
251
+ };
252
+ // Format agent feed
253
+ const feed = actions.slice(0, 20).map((act) => {
254
+ const who = act.agent_name ?? act.agent_id ?? "agent";
255
+ const what = (act.action_type ?? act.type ?? "action").toLowerCase();
256
+ const emoji = ACTION_EMOJI[what] ?? "•";
257
+ const content = act.content ?? act.text ?? "";
258
+ return `${emoji} **${who}** [${what}]${content ? `: ${String(content).slice(0, 120)}` : ""}`;
259
+ });
260
+ // Generate AI brief from agent activity
261
+ let brief = "";
262
+ if (actions.length > 0 && (process.env.ANTHROPIC_API_KEY || process.env.BANKR_API_KEY)) {
263
+ const activitySummary = actions.slice(0, 30).map((act) => {
254
264
  const who = act.agent_name ?? act.agent_id ?? "agent";
255
265
  const what = act.action_type ?? act.type ?? "action";
256
266
  const content = act.content ?? act.text ?? "";
257
- lines.push(`• **${who}** [${what}]${content ? `: ${String(content).slice(0, 80)}` : ""}`);
267
+ return `${who} [${what}]: ${String(content).slice(0, 150)}`;
268
+ }).join("\n");
269
+ try {
270
+ brief = await (0, llm_js_1.callLLM)("You are a market intelligence analyst. Given a MiroShark multi-agent simulation log, extract: 1) Key market sentiment, 2) Dominant narrative, 3) Top 3 agent behaviors, 4) Outlook. Be concise and direct — max 150 words.", `Simulation activity log:\n${activitySummary}`, 400);
271
+ }
272
+ catch {
273
+ // brief stays empty — non-critical
274
+ }
275
+ }
276
+ // Auto-save to vault if key is available
277
+ const savedToVault = false;
278
+ if (brief) {
279
+ try {
280
+ await (0, convex_js_1.callConvex)("/vault/save", "POST", {
281
+ key: `miroshark-${simId.slice(0, 8)}`,
282
+ value: brief,
283
+ tags: ["miroshark", "simulation", "research"],
284
+ }, "vault_save");
258
285
  }
286
+ catch {
287
+ // non-critical
288
+ }
289
+ }
290
+ const lines = [
291
+ `**MiroShark \`${simId}\`** — ${runnerStatus}`,
292
+ `Rounds: ${rounds} · Actions: ${totalActions} agents`,
293
+ "",
294
+ ];
295
+ if (brief) {
296
+ lines.push("**🧠 AI Brief:**", brief, "");
297
+ }
298
+ if (feed.length > 0) {
299
+ lines.push(`**Agent Feed** (${Math.min(actions.length, 20)} of ${totalActions}):`);
300
+ lines.push(...feed);
301
+ }
302
+ if (brief) {
303
+ lines.push("", `_Findings auto-saved to vault as \`miroshark-${simId.slice(0, 8)}\`_`);
259
304
  }
260
- lines.push("", `Full transcript: \`miroshark_status\` returns results above.`);
261
305
  return { content: [{ type: "text", text: lines.join("\n") }] };
262
306
  }
263
307
  // Fallback: unknown state