@finchagentic/mcp 4.1.0 → 4.4.1

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.
Files changed (44) hide show
  1. package/README.md +19 -15
  2. package/dist/agent-loop.js +75 -72
  3. package/dist/annotations.js +23 -14
  4. package/dist/convex.js +15 -18
  5. package/dist/index.js +14 -12
  6. package/dist/llm.js +12 -1
  7. package/dist/local-memory-file.js +14 -1
  8. package/dist/local-memory.js +13 -0
  9. package/dist/output-schemas.js +71 -17
  10. package/dist/project.js +36 -0
  11. package/dist/resources.js +6 -11
  12. package/dist/server.js +35 -13
  13. package/dist/token-gate.js +2 -2
  14. package/dist/tool-filter.js +14 -5
  15. package/dist/tools/agents.js +106 -394
  16. package/dist/tools/automation.js +42 -6
  17. package/dist/tools/base-mcp.js +3 -15
  18. package/dist/tools/base.js +34 -20
  19. package/dist/tools/coder.js +1 -1
  20. package/dist/tools/deep-research.js +1 -1
  21. package/dist/tools/defi.js +14 -34
  22. package/dist/tools/equity.js +10 -2
  23. package/dist/tools/events.js +1 -1
  24. package/dist/tools/github.js +51 -1
  25. package/dist/tools/insider.js +1 -1
  26. package/dist/tools/insight.js +4 -4
  27. package/dist/tools/market.js +5 -5
  28. package/dist/tools/memory.js +52 -88
  29. package/dist/tools/miroshark.js +8 -1
  30. package/dist/tools/monitor.js +8 -8
  31. package/dist/tools/os.js +9 -4
  32. package/dist/tools/packets.js +2 -2
  33. package/dist/tools/research-chain.js +1 -1
  34. package/dist/tools/research-compare.js +1 -1
  35. package/dist/tools/research.js +2 -2
  36. package/dist/tools/rh-bridge.js +1 -1
  37. package/dist/tools/rh-mcp.js +29 -4
  38. package/dist/tools/rh-orders.js +201 -123
  39. package/dist/tools/scanner.js +33 -3
  40. package/dist/tools/stake.js +369 -0
  41. package/dist/tools/vault.js +294 -40
  42. package/dist/wallet.js +130 -19
  43. package/package.json +4 -5
  44. package/dist/tools/framework.js +0 -150
@@ -56,6 +56,53 @@ function saveStore(s) {
56
56
  function genId(prefix) {
57
57
  return `${prefix}_${Date.now().toString(36)}${Math.floor(Math.random() * 1e6).toString(36)}`;
58
58
  }
59
+ // ── Tick lock ──────────────────────────────────────────────────────────────
60
+ // rh_orders_tick reads the store, awaits a real on-chain swap per due order
61
+ // (a slow network round trip), and only writes the store back at the end.
62
+ // Without a lock, two overlapping executing ticks (a slow RPC making one call
63
+ // outlast the next scheduler firing, or a manual call racing the scheduler)
64
+ // both read the SAME pre-update state, both see the same order as due, and
65
+ // both broadcast a real swap - real funds spent twice for what the store
66
+ // ends up recording as one buy. Preview ticks (execute:false) never write and
67
+ // are harmless to run concurrently, so only executing ticks take the lock.
68
+ const TICK_LOCK_FILE = path_1.default.join(ORDERS_DIR, "rh-orders.tick.lock");
69
+ const LOCK_STALE_MS = 5 * 60 * 1000; // a tick this slow is presumed crashed, not just slow
70
+ function acquireTickLock() {
71
+ try {
72
+ fs_1.default.mkdirSync(ORDERS_DIR, { recursive: true });
73
+ }
74
+ catch {
75
+ /* ignore */
76
+ }
77
+ try {
78
+ // Exclusive create - atomically fails if another tick already holds it.
79
+ fs_1.default.writeFileSync(TICK_LOCK_FILE, String(process.pid), { flag: "wx" });
80
+ return true;
81
+ }
82
+ catch {
83
+ try {
84
+ const stat = fs_1.default.statSync(TICK_LOCK_FILE);
85
+ if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
86
+ // Prior holder almost certainly crashed without releasing - take over
87
+ // rather than deadlocking every future tick forever.
88
+ fs_1.default.writeFileSync(TICK_LOCK_FILE, String(process.pid));
89
+ return true;
90
+ }
91
+ }
92
+ catch {
93
+ /* lock vanished between the failed create and this stat - try again next tick */
94
+ }
95
+ return false;
96
+ }
97
+ }
98
+ function releaseTickLock() {
99
+ try {
100
+ fs_1.default.unlinkSync(TICK_LOCK_FILE);
101
+ }
102
+ catch {
103
+ /* already gone */
104
+ }
105
+ }
59
106
  function killSwitchOn() {
60
107
  return process.env.RH_ORDERS_DISABLED === "1" || fs_1.default.existsSync(KILL_FILE);
61
108
  }
@@ -63,6 +110,18 @@ function posNum(v) {
63
110
  const n = Number(v);
64
111
  return isFinite(n) ? n : NaN;
65
112
  }
113
+ // Same bound quoteRh() enforces in rh-mcp.ts (matching defi.ts's Base-swap
114
+ // bound of .positive().max(50)) - reject an out-of-range slippagePct at order
115
+ // creation time rather than letting it silently reach an unattended
116
+ // rh_orders_tick execution weeks later.
117
+ function badSlippagePct(v) {
118
+ if (v == null)
119
+ return null;
120
+ const n = posNum(v);
121
+ if (!(n > 0) || n > 50)
122
+ return `slippagePct must be greater than 0 and at most 50 (got ${v})`;
123
+ return null;
124
+ }
66
125
  function fmtUsd(n) {
67
126
  if (n == null || !isFinite(n))
68
127
  return "?";
@@ -192,6 +251,9 @@ async function handleRhOrderTool(name, args) {
192
251
  return textResult("intervalHours must be > 0", true);
193
252
  if (!(totalBuys > 0))
194
253
  return textResult("totalBuys must be ≥ 1", true);
254
+ const slippageErr = badSlippagePct(a.slippagePct);
255
+ if (slippageErr)
256
+ return textResult(slippageErr, true);
195
257
  let resolved;
196
258
  try {
197
259
  resolved = await (0, rh_mcp_js_1.resolveTokenSmart)(String(a.token));
@@ -257,6 +319,9 @@ async function handleRhOrderTool(name, args) {
257
319
  const sellPct = a.sellPct != null ? posNum(a.sellPct) : 100;
258
320
  if (!(sellPct > 0 && sellPct <= 100))
259
321
  return textResult("sellPct must be 1..100", true);
322
+ const bracketSlippageErr = badSlippagePct(a.slippagePct);
323
+ if (bracketSlippageErr)
324
+ return textResult(bracketSlippageErr, true);
260
325
  let resolved;
261
326
  try {
262
327
  resolved = await (0, rh_mcp_js_1.resolveTokenSmart)(String(a.token));
@@ -327,143 +392,156 @@ async function handleRhOrderTool(name, args) {
327
392
  case "rh_orders_tick": {
328
393
  const execute = a.execute === true;
329
394
  const blocked = execute && killSwitchOn();
330
- const store = loadStore();
331
- const active = store.orders.filter((o) => o.status === "active");
332
- const now = Date.now();
333
- const report = [
334
- `## ⏱️ RH orders tick — ${execute ? (blocked ? "EXECUTE (BLOCKED by kill-switch → preview)" : "EXECUTE") : "PREVIEW"}`,
335
- `${active.length} active order(s)`,
336
- "",
337
- ];
338
395
  const doExecute = execute && !blocked;
339
- let acted = 0;
340
- let wallet = null;
341
- for (const o of active) {
342
- try {
343
- // ---- DCA ----
344
- if (o.type === "dca" && o.dca) {
345
- const d = o.dca;
346
- if (d.completedBuys >= d.totalBuys) {
347
- o.status = "completed";
348
- continue;
349
- }
350
- if (now < d.nextRunAt) {
351
- report.push(`⏳ ${o.id} DCA ${o.token.symbol}: next buy at ${new Date(d.nextRunAt).toISOString()}`);
352
- continue;
353
- }
354
- const wouldSpend = Number(d.spentEth) + Number(d.amountEthPerBuy);
355
- if (wouldSpend > Number(d.maxSpendEth) + 1e-12) {
356
- o.status = "completed";
357
- report.push(`✅ ${o.id} DCA ${o.token.symbol}: maxSpend reached — completed`);
358
- continue;
359
- }
360
- acted++;
361
- if (!doExecute) {
362
- report.push(`🟡 ${o.id} DCA: would buy ${d.amountEthPerBuy} ETH → ${o.token.symbol} (${d.completedBuys + 1}/${d.totalBuys})`);
363
- continue;
364
- }
365
- const res = await (0, rh_mcp_js_1.handleRhMcpTool)("rh_mcp_swap", {
366
- fromToken: "ETH",
367
- toToken: o.token.address,
368
- amount: d.amountEthPerBuy,
369
- maxSlippagePct: o.slippagePct,
370
- confirm: true,
371
- });
372
- const txt = res?.content?.[0]?.text ?? "";
373
- if (res?.isError) {
374
- o.lastError = txt.slice(0, 140);
396
+ // Only an executing tick can double-spend; a preview never writes and
397
+ // is harmless to overlap, so only take the lock when it matters.
398
+ if (doExecute && !acquireTickLock()) {
399
+ return textResult("⏳ Another rh_orders_tick is already executing - refusing to run a second one " +
400
+ "concurrently (this is what prevents a slow tick from double-firing the same order). " +
401
+ "Try again shortly.", true);
402
+ }
403
+ try {
404
+ const store = loadStore();
405
+ const active = store.orders.filter((o) => o.status === "active");
406
+ const now = Date.now();
407
+ const report = [
408
+ `## ⏱️ RH orders tick — ${execute ? (blocked ? "EXECUTE (BLOCKED by kill-switch → preview)" : "EXECUTE") : "PREVIEW"}`,
409
+ `${active.length} active order(s)`,
410
+ "",
411
+ ];
412
+ let acted = 0;
413
+ let wallet = null;
414
+ for (const o of active) {
415
+ try {
416
+ // ---- DCA ----
417
+ if (o.type === "dca" && o.dca) {
418
+ const d = o.dca;
419
+ if (d.completedBuys >= d.totalBuys) {
420
+ o.status = "completed";
421
+ continue;
422
+ }
423
+ if (now < d.nextRunAt) {
424
+ report.push(`⏳ ${o.id} DCA ${o.token.symbol}: next buy at ${new Date(d.nextRunAt).toISOString()}`);
425
+ continue;
426
+ }
427
+ const wouldSpend = Number(d.spentEth) + Number(d.amountEthPerBuy);
428
+ if (wouldSpend > Number(d.maxSpendEth) + 1e-12) {
429
+ o.status = "completed";
430
+ report.push(`✅ ${o.id} DCA ${o.token.symbol}: maxSpend reached — completed`);
431
+ continue;
432
+ }
433
+ acted++;
434
+ if (!doExecute) {
435
+ report.push(`🟡 ${o.id} DCA: would buy ${d.amountEthPerBuy} ETH → ${o.token.symbol} (${d.completedBuys + 1}/${d.totalBuys})`);
436
+ continue;
437
+ }
438
+ const res = await (0, rh_mcp_js_1.handleRhMcpTool)("rh_mcp_swap", {
439
+ fromToken: "ETH",
440
+ toToken: o.token.address,
441
+ amount: d.amountEthPerBuy,
442
+ maxSlippagePct: o.slippagePct,
443
+ confirm: true,
444
+ });
445
+ const txt = res?.content?.[0]?.text ?? "";
446
+ if (res?.isError) {
447
+ o.lastError = txt.slice(0, 140);
448
+ o.updatedAt = now;
449
+ report.push(`🔴 ${o.id} DCA buy failed: ${o.lastError}`);
450
+ continue;
451
+ }
452
+ const txHash = extractTxHash(txt) ?? "unknown";
453
+ d.fills.push({ ts: now, txHash, amountEth: d.amountEthPerBuy });
454
+ d.completedBuys += 1;
455
+ d.spentEth = String(Number(d.spentEth) + Number(d.amountEthPerBuy));
456
+ d.nextRunAt = now + d.intervalSec * 1000; // no burst catch-up
457
+ o.lastError = undefined;
375
458
  o.updatedAt = now;
376
- report.push(`🔴 ${o.id} DCA buy failed: ${o.lastError}`);
459
+ if (d.completedBuys >= d.totalBuys)
460
+ o.status = "completed";
461
+ report.push(`🟢 ${o.id} DCA bought ${d.amountEthPerBuy} ETH → ${o.token.symbol} · ${d.completedBuys}/${d.totalBuys} · tx \`${txHash}\``);
377
462
  continue;
378
463
  }
379
- const txHash = extractTxHash(txt) ?? "unknown";
380
- d.fills.push({ ts: now, txHash, amountEth: d.amountEthPerBuy });
381
- d.completedBuys += 1;
382
- d.spentEth = String(Number(d.spentEth) + Number(d.amountEthPerBuy));
383
- d.nextRunAt = now + d.intervalSec * 1000; // no burst catch-up
384
- o.lastError = undefined;
385
- o.updatedAt = now;
386
- if (d.completedBuys >= d.totalBuys)
464
+ // ---- Bracket (TP/SL) ----
465
+ if (o.type === "bracket" && o.bracket) {
466
+ const b = o.bracket;
467
+ const price = await (0, rh_mcp_js_1.rhPriceUsd)(o.token.address);
468
+ if (price == null) {
469
+ report.push(`⚠️ ${o.id} TP/SL ${o.token.symbol}: no price available`);
470
+ continue;
471
+ }
472
+ let trigger = null;
473
+ if (b.tpPriceUsd != null && price >= b.tpPriceUsd)
474
+ trigger = "tp";
475
+ else if (b.slPriceUsd != null && price <= b.slPriceUsd)
476
+ trigger = "sl";
477
+ if (!trigger) {
478
+ report.push(`⏳ ${o.id} TP/SL ${o.token.symbol}: ${fmtUsd(price)} (🎯${b.tpPriceUsd != null ? fmtUsd(b.tpPriceUsd) : "—"} / 🛑${b.slPriceUsd != null ? fmtUsd(b.slPriceUsd) : "—"})`);
479
+ continue;
480
+ }
481
+ if (!wallet)
482
+ wallet = await (0, wallet_js_1.getOrCreateWallet)();
483
+ const balRaw = await (0, rh_mcp_js_1.rhErc20Balance)(o.token.address, wallet.address);
484
+ if (balRaw <= 0n) {
485
+ o.status = "completed";
486
+ report.push(`✅ ${o.id} TP/SL ${o.token.symbol}: ${trigger.toUpperCase()} hit but 0 balance — completed`);
487
+ continue;
488
+ }
489
+ const sellRaw = (balRaw * BigInt(Math.round(b.sellPct * 100))) / 10000n;
490
+ const sellHuman = ethers_1.ethers.formatUnits(sellRaw, o.token.decimals);
491
+ acted++;
492
+ if (!doExecute) {
493
+ report.push(`🟡 ${o.id} ${trigger.toUpperCase()} at ${fmtUsd(price)}: would sell ${b.sellPct}% (${sellHuman} ${o.token.symbol}) → ETH`);
494
+ continue;
495
+ }
496
+ const res = await (0, rh_mcp_js_1.handleRhMcpTool)("rh_mcp_swap", {
497
+ fromToken: o.token.address,
498
+ toToken: "ETH",
499
+ amount: sellHuman,
500
+ maxSlippagePct: o.slippagePct,
501
+ confirm: true,
502
+ });
503
+ const txt = res?.content?.[0]?.text ?? "";
504
+ if (res?.isError) {
505
+ o.lastError = txt.slice(0, 140);
506
+ o.updatedAt = now;
507
+ report.push(`🔴 ${o.id} ${trigger.toUpperCase()} sell failed: ${o.lastError}`);
508
+ continue;
509
+ }
510
+ const txHash = extractTxHash(txt) ?? "unknown";
511
+ b.fills.push({ ts: now, txHash, kind: trigger, priceUsd: price });
387
512
  o.status = "completed";
388
- report.push(`🟢 ${o.id} DCA bought ${d.amountEthPerBuy} ETH → ${o.token.symbol} · ${d.completedBuys}/${d.totalBuys} · tx \`${txHash}\``);
389
- continue;
390
- }
391
- // ---- Bracket (TP/SL) ----
392
- if (o.type === "bracket" && o.bracket) {
393
- const b = o.bracket;
394
- const price = await (0, rh_mcp_js_1.rhPriceUsd)(o.token.address);
395
- if (price == null) {
396
- report.push(`⚠️ ${o.id} TP/SL ${o.token.symbol}: no price available`);
397
- continue;
398
- }
399
- let trigger = null;
400
- if (b.tpPriceUsd != null && price >= b.tpPriceUsd)
401
- trigger = "tp";
402
- else if (b.slPriceUsd != null && price <= b.slPriceUsd)
403
- trigger = "sl";
404
- if (!trigger) {
405
- report.push(`⏳ ${o.id} TP/SL ${o.token.symbol}: ${fmtUsd(price)} (🎯${b.tpPriceUsd != null ? fmtUsd(b.tpPriceUsd) : "—"} / 🛑${b.slPriceUsd != null ? fmtUsd(b.slPriceUsd) : "—"})`);
406
- continue;
407
- }
408
- if (!wallet)
409
- wallet = await (0, wallet_js_1.getOrCreateWallet)();
410
- const balRaw = await (0, rh_mcp_js_1.rhErc20Balance)(o.token.address, wallet.address);
411
- if (balRaw <= 0n) {
412
- o.status = "completed";
413
- report.push(`✅ ${o.id} TP/SL ${o.token.symbol}: ${trigger.toUpperCase()} hit but 0 balance — completed`);
414
- continue;
415
- }
416
- const sellRaw = (balRaw * BigInt(Math.round(b.sellPct * 100))) / 10000n;
417
- const sellHuman = ethers_1.ethers.formatUnits(sellRaw, o.token.decimals);
418
- acted++;
419
- if (!doExecute) {
420
- report.push(`🟡 ${o.id} ${trigger.toUpperCase()} at ${fmtUsd(price)}: would sell ${b.sellPct}% (${sellHuman} ${o.token.symbol}) → ETH`);
421
- continue;
422
- }
423
- const res = await (0, rh_mcp_js_1.handleRhMcpTool)("rh_mcp_swap", {
424
- fromToken: o.token.address,
425
- toToken: "ETH",
426
- amount: sellHuman,
427
- maxSlippagePct: o.slippagePct,
428
- confirm: true,
429
- });
430
- const txt = res?.content?.[0]?.text ?? "";
431
- if (res?.isError) {
432
- o.lastError = txt.slice(0, 140);
513
+ o.lastError = undefined;
433
514
  o.updatedAt = now;
434
- report.push(`🔴 ${o.id} ${trigger.toUpperCase()} sell failed: ${o.lastError}`);
515
+ report.push(`🟢 ${o.id} ${trigger.toUpperCase()} sold ${sellHuman} ${o.token.symbol} → ETH at ${fmtUsd(price)} · tx \`${txHash}\``);
435
516
  continue;
436
517
  }
437
- const txHash = extractTxHash(txt) ?? "unknown";
438
- b.fills.push({ ts: now, txHash, kind: trigger, priceUsd: price });
439
- o.status = "completed";
440
- o.lastError = undefined;
518
+ }
519
+ catch (e) {
520
+ // One bad order (e.g. wallet decrypt failure) must not abort the whole tick.
521
+ o.lastError = String(e?.message ?? e).slice(0, 140);
441
522
  o.updatedAt = now;
442
- report.push(`🟢 ${o.id} ${trigger.toUpperCase()} sold ${sellHuman} ${o.token.symbol} → ETH at ${fmtUsd(price)} · tx \`${txHash}\``);
443
- continue;
523
+ report.push(`🔴 ${o.id}: tick error ${o.lastError}`);
444
524
  }
445
525
  }
446
- catch (e) {
447
- // One bad order (e.g. wallet decrypt failure) must not abort the whole tick.
448
- o.lastError = String(e?.message ?? e).slice(0, 140);
449
- o.updatedAt = now;
450
- report.push(`🔴 ${o.id}: tick error${o.lastError}`);
526
+ // Preview must be read-only: only persist state when actually executing.
527
+ if (doExecute)
528
+ saveStore(store);
529
+ if (blocked) {
530
+ report.push("", "🛑 Kill-switch activeno swaps broadcast. Remove ~/.finch/rh-orders.OFF or unset RH_ORDERS_DISABLED to enable.");
451
531
  }
532
+ else if (acted === 0) {
533
+ report.push("", "Nothing due or triggered this tick.");
534
+ }
535
+ else if (!doExecute) {
536
+ report.push("", `${acted} action(s) pending. Re-run with \`execute:true\` (via scheduler) to broadcast.`);
537
+ }
538
+ report.push("", `_Explorer: ${rh_mcp_js_1.RH_EXPLORER}_`);
539
+ return textResult(report.join("\n"));
452
540
  }
453
- // Preview must be read-only: only persist state when actually executing.
454
- if (doExecute)
455
- saveStore(store);
456
- if (blocked) {
457
- report.push("", "🛑 Kill-switch active — no swaps broadcast. Remove ~/.finch/rh-orders.OFF or unset RH_ORDERS_DISABLED to enable.");
458
- }
459
- else if (acted === 0) {
460
- report.push("", "Nothing due or triggered this tick.");
461
- }
462
- else if (!doExecute) {
463
- report.push("", `${acted} action(s) pending. Re-run with \`execute:true\` (via scheduler) to broadcast.`);
541
+ finally {
542
+ if (doExecute)
543
+ releaseTickLock();
464
544
  }
465
- report.push("", `_Explorer: ${rh_mcp_js_1.RH_EXPLORER}_`);
466
- return textResult(report.join("\n"));
467
545
  }
468
546
  default:
469
547
  return null;
@@ -134,6 +134,18 @@ function buildScanResults(mode, scanned, scored) {
134
134
  // from the check_token handler so the text report and structuredContent can't
135
135
  // drift apart.
136
136
  function assessTokenSecurity(info) {
137
+ // GoPlusLabs returns HTTP 200 with an empty {} for a contract it has no
138
+ // record of (too new to be indexed yet, wrong address, etc.) - every field
139
+ // below defaults to "not risky" in that case, which previously produced a
140
+ // rugScore of 20 and a "SAFE" verdict for a token that was NEVER actually
141
+ // scanned. That's the exact class of token this tool exists to protect
142
+ // against (score_token/scan_market point brand-new tokens here first).
143
+ if (!info || Object.keys(info).length === 0) {
144
+ return {
145
+ verdict: "UNSCANNED", rugScore: -1, isHoneypot: false, isMintable: false,
146
+ isFreezeAuth: false, isOpenSource: false, lpLockedPct: 0, buyTax: 0, sellTax: 0, holderCount: null,
147
+ };
148
+ }
137
149
  const isHoneypot = info.is_honeypot === "1";
138
150
  const isMintable = info.is_mintable === "1";
139
151
  const isFreezeAuth = info.transfer_pausable === "1";
@@ -333,7 +345,7 @@ async function handleScannerTool(name, args) {
333
345
  if (name === "score_token") {
334
346
  const parsed = ScoreTokenSchema.safeParse(args);
335
347
  if (!parsed.success)
336
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
348
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
337
349
  const { address, minLiquidity = DEFAULT_MIN_LIQ } = parsed.data;
338
350
  const data = await fetchJson(`https://api.dexscreener.com/latest/dex/tokens/${address}`);
339
351
  const pair = (data.pairs ?? [])
@@ -402,13 +414,31 @@ async function handleScannerTool(name, args) {
402
414
  if (name === "check_token") {
403
415
  const parsed = CheckTokenSchema.safeParse(args);
404
416
  if (!parsed.success)
405
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
417
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
406
418
  const { address } = parsed.data;
407
419
  // GoPlusLabs - chain 8453 = Base mainnet
408
420
  const data = await fetchJson(`https://api.gopluslabs.io/api/v1/token_security/8453?contract_addresses=${address}`);
409
421
  const info = data.result?.[address.toLowerCase()] ?? data.result?.[address] ?? {};
410
422
  const sec = assessTokenSecurity(info);
411
423
  const { verdict, rugScore, isHoneypot, isMintable, isFreezeAuth, isOpenSource, lpLockedPct, buyTax, sellTax } = sec;
424
+ if (verdict === "UNSCANNED") {
425
+ return {
426
+ content: [{
427
+ type: "text",
428
+ text: [
429
+ `## Token Security Check`,
430
+ `\`${address}\``,
431
+ ``,
432
+ `**Verdict: ⚪ UNSCANNED** - GoPlusLabs has no record for this contract (too new to be indexed yet, or the address is wrong).`,
433
+ ``,
434
+ `This is NOT a clean bill of health - it means no security data exists to check at all. ` +
435
+ `Brand-new tokens (exactly what \`scan_market\` surfaces) are the most likely to hit this. ` +
436
+ `Treat as unverified: confirm the contract address is correct, wait and retry once the token is a few hours/days old, or skip it.`,
437
+ ].join("\n"),
438
+ }],
439
+ structuredContent: { address, ...sec },
440
+ };
441
+ }
412
442
  const icon = { DANGER: "🔴", CAUTION: "🟡", SAFE: "🟢" }[verdict];
413
443
  const lines = [
414
444
  `## Token Security Check`,
@@ -446,7 +476,7 @@ async function handleScannerTool(name, args) {
446
476
  if (name === "scan_market") {
447
477
  const parsed = ScanDipsSchema.safeParse(args ?? {});
448
478
  if (!parsed.success)
449
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
479
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
450
480
  const input = args;
451
481
  const mode = input?.mode === "momentum" ? "momentum" : "dips";
452
482
  const { minScore = DEFAULT_MIN_SCORE, minLiquidity = DEFAULT_MIN_LIQ, limit = 40 } = parsed.data;