@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.
package/dist/convex.js CHANGED
@@ -54,7 +54,9 @@ async function callConvex(path, method, body, toolName = "unknown") {
54
54
  const paymentHeader = process.env.NOELCLAW_PAYMENT_HEADER;
55
55
  if (paymentHeader)
56
56
  headers["X-Payment"] = paymentHeader;
57
- // BYOK headers
57
+ // BYOK headers — user pays for their own AI/service costs
58
+ if (process.env.ANTHROPIC_API_KEY)
59
+ headers["X-User-Anthropic-Key"] = process.env.ANTHROPIC_API_KEY;
58
60
  if (process.env.GROK_API_KEY)
59
61
  headers["X-User-Grok-Key"] = process.env.GROK_API_KEY;
60
62
  if (process.env.BANKR_API_KEY)
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/llm.js ADDED
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.callLLM = callLLM;
4
+ const ANTHROPIC_URL = "https://api.anthropic.com/v1/messages";
5
+ const BANKR_URL = "https://llm.bankr.bot/v1/chat/completions";
6
+ /**
7
+ * Call the best available LLM.
8
+ * Priority: ANTHROPIC_API_KEY → BANKR_API_KEY → throws
9
+ * Claude Desktop automatically injects ANTHROPIC_API_KEY, so users
10
+ * pay for their own AI usage without the server owner absorbing the cost.
11
+ */
12
+ async function callLLM(systemPrompt, userPrompt, maxTokens = 1024, history = [], timeoutMs = 60000) {
13
+ const anthropicKey = process.env.ANTHROPIC_API_KEY;
14
+ const bankrKey = process.env.BANKR_API_KEY;
15
+ if (anthropicKey)
16
+ return callAnthropic(anthropicKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs);
17
+ if (bankrKey)
18
+ return callBankr(bankrKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs);
19
+ throw new Error("No LLM API key available. Claude Desktop users: make sure ANTHROPIC_API_KEY is in your MCP config. " +
20
+ "Standalone users: set BANKR_API_KEY.");
21
+ }
22
+ async function callAnthropic(apiKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs) {
23
+ const messages = [...history, { role: "user", content: userPrompt }];
24
+ const model = process.env.ANTHROPIC_MODEL ?? "claude-haiku-4-5-20251001";
25
+ const res = await fetch(ANTHROPIC_URL, {
26
+ method: "POST",
27
+ headers: {
28
+ "Content-Type": "application/json",
29
+ "x-api-key": apiKey,
30
+ "anthropic-version": "2023-06-01",
31
+ },
32
+ body: JSON.stringify({ model, max_tokens: maxTokens, system: systemPrompt, messages }),
33
+ signal: AbortSignal.timeout(timeoutMs),
34
+ });
35
+ if (!res.ok) {
36
+ const body = await res.text().catch(() => "");
37
+ throw new Error(`Anthropic error ${res.status}: ${body.slice(0, 200)}`);
38
+ }
39
+ const data = await res.json();
40
+ return data.content?.find(b => b.type === "text")?.text ?? "";
41
+ }
42
+ async function callBankr(apiKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs) {
43
+ const model = process.env.BANKR_MODEL ?? "grok-3";
44
+ const res = await fetch(BANKR_URL, {
45
+ method: "POST",
46
+ headers: { "Content-Type": "application/json", "X-API-Key": apiKey },
47
+ body: JSON.stringify({
48
+ model,
49
+ messages: [
50
+ { role: "system", content: systemPrompt },
51
+ ...history,
52
+ { role: "user", content: userPrompt },
53
+ ],
54
+ max_tokens: maxTokens,
55
+ }),
56
+ signal: AbortSignal.timeout(timeoutMs),
57
+ });
58
+ if (!res.ok) {
59
+ const body = await res.text().catch(() => "");
60
+ throw new Error(`Bankr error ${res.status}: ${body.slice(0, 200)}`);
61
+ }
62
+ const data = await res.json();
63
+ return data.choices?.[0]?.message?.content ?? "";
64
+ }
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,20 +34,23 @@ 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, // 9 — start, stop, status, read/write memory, scores, research, brief, trigger_agent
41
42
  ...framework_js_1.FRAMEWORK_TOOLS, // 6 — task packets, playbooks, sentinel, ledger
42
- ...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
43
+ ...vault_js_1.VAULT_TOOLS, // 13 — save, read, list, search, history, diff, export, remember, context, store_credential, get_credential, publish, explore
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: 62
48
52
  ];
49
- exports.server = new index_js_1.Server({ name: "noelclaw", version: "2.1.0" }, { capabilities: { tools: {} } });
53
+ exports.server = new index_js_1.Server({ name: "noelclaw", version: "2.2.0" }, { capabilities: { tools: {} } });
50
54
  exports.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({ tools: exports.ALL_TOOLS }));
51
55
  exports.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
52
56
  const { name, arguments: args } = request.params;
@@ -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
  }