@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.
@@ -0,0 +1,375 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SCANNER_TOOLS = void 0;
4
+ exports.handleScannerTool = handleScannerTool;
5
+ const zod_1 = require("zod");
6
+ // ── Constants ─────────────────────────────────────────────────────────────────
7
+ const DEFAULT_MIN_LIQ = 50000;
8
+ const DEFAULT_MIN_SCORE = 50;
9
+ // ── Dip-reversal scorer ───────────────────────────────────────────────────────
10
+ // Ported 1:1 from circuit-agent/lib/scoring.js (battle-tested, pure math).
11
+ function scoreDipReversal(c, minLiquidity = DEFAULT_MIN_LIQ) {
12
+ const pc5m = c.priceChange5m;
13
+ const pc1h = c.priceChange1h;
14
+ const pc6h = c.priceChange6h;
15
+ const pc24h = c.priceChange24h;
16
+ const liq = c.liquidity;
17
+ const vol1h = c.volume1h;
18
+ const totalTxns5m = c.buys5m + c.sells5m;
19
+ const buyRatio5m = totalTxns5m > 0 ? c.buys5m / totalTxns5m : 0;
20
+ const totalTxns1h = c.buys1h + c.sells1h;
21
+ const buyRatio1h = totalTxns1h > 0 ? c.buys1h / totalTxns1h : 0;
22
+ // ── Hard gates — all must pass ────────────────────────────────────────────
23
+ const gateFailures = [];
24
+ if (pc1h >= 0)
25
+ gateFailures.push(`1h not negative (${pc1h.toFixed(1)}%)`);
26
+ if (pc5m < 0.5)
27
+ gateFailures.push(`5m bounce weak (${pc5m.toFixed(1)}% < 0.5%)`);
28
+ if (totalTxns5m > 5 && buyRatio5m <= 0.50)
29
+ gateFailures.push(`buy ratio low (${(buyRatio5m * 100).toFixed(0)}%)`);
30
+ if (liq < minLiquidity)
31
+ gateFailures.push(`liquidity $${(liq / 1000).toFixed(0)}k < $${(minLiquidity / 1000).toFixed(0)}k min`);
32
+ if (pc6h <= -20 && pc24h <= -20)
33
+ gateFailures.push(`dead cat (6h ${pc6h.toFixed(0)}% / 24h ${pc24h.toFixed(0)}%)`);
34
+ if (gateFailures.length > 0) {
35
+ return { score: 0, passed: false, pattern: null, breakdown: {}, gateFailures, buyPressure5m: buyRatio5m * 100 };
36
+ }
37
+ // ── 1. Drop depth (0–25 pts) — deeper dip = more bounce room ─────────────
38
+ const dropPts = pc1h <= -10 ? 25 : pc1h <= -5 ? 20 : pc1h <= -3 ? 15 : 5;
39
+ // ── 2. Bounce confirmation (0–20 pts) ─────────────────────────────────────
40
+ const bouncePts = pc5m >= 5 ? 20 : pc5m >= 3 ? 17 : pc5m >= 2 ? 14 : pc5m >= 1 ? 10 : 5;
41
+ // ── 3. Sentiment shift (0–15 pts) — buyers returning after selloff ────────
42
+ const sentimentShift = buyRatio5m - buyRatio1h;
43
+ const sentPts = sentimentShift >= 0.10 ? 15
44
+ : sentimentShift >= 0.05 ? 10
45
+ : sentimentShift >= 0.02 ? 7
46
+ : sentimentShift > 0 ? 3 : 0;
47
+ // ── 4. Buy pressure (0–10 pts) ────────────────────────────────────────────
48
+ const bp = buyRatio5m * 100;
49
+ const bpPts = bp >= 65 ? 10 : bp >= 58 ? 8 : bp >= 53 ? 5 : 2;
50
+ // ── 5. Volume & activity (0–15 pts) — validates bounce is real ───────────
51
+ const actPts = vol1h >= 100000 && totalTxns1h >= 200 ? 15
52
+ : vol1h >= 50000 && totalTxns1h >= 100 ? 12
53
+ : vol1h >= 20000 && totalTxns1h >= 40 ? 8
54
+ : vol1h >= 5000 && totalTxns1h >= 10 ? 4 : 1;
55
+ // ── 6. Trend alignment (−10 to +15 pts) — dip in uptrend vs dead cat ─────
56
+ let trendPts;
57
+ if (pc6h > 0 && pc24h > 0)
58
+ trendPts = 15;
59
+ else if (pc24h > 0)
60
+ trendPts = 10;
61
+ else if (pc6h > 0)
62
+ trendPts = 5;
63
+ else {
64
+ const avg = (pc6h + pc24h) / 2;
65
+ trendPts = avg <= -15 ? -10 : avg <= -8 ? -7 : avg <= -4 ? -5 : -2;
66
+ }
67
+ const score = Math.max(0, Math.min(100, dropPts + bouncePts + sentPts + bpPts + actPts + trendPts));
68
+ const pattern = pc1h < -10 ? "DEEP-REVERSAL"
69
+ : pc1h < -5 ? "REVERSAL"
70
+ : pc1h < -3 ? "DIP-BUY"
71
+ : "SHALLOW-DIP";
72
+ return {
73
+ score,
74
+ passed: true,
75
+ pattern,
76
+ breakdown: {
77
+ dropDepth: { value: +pc1h.toFixed(1), points: dropPts },
78
+ bounce: { value: +pc5m.toFixed(1), points: bouncePts },
79
+ sentimentShift: { value: +sentimentShift.toFixed(2), points: sentPts },
80
+ buyPressure: { value: +bp.toFixed(0), points: bpPts },
81
+ activity: { vol1h, txns1h: totalTxns1h, points: actPts },
82
+ trendAlignment: { pc6h: +pc6h.toFixed(1), pc24h: +pc24h.toFixed(1), points: trendPts },
83
+ },
84
+ gateFailures: [],
85
+ buyPressure5m: bp,
86
+ };
87
+ }
88
+ // ── Fetch helper ──────────────────────────────────────────────────────────────
89
+ async function fetchJson(url) {
90
+ const res = await fetch(url, { signal: AbortSignal.timeout(15000) });
91
+ if (!res.ok)
92
+ throw new Error(`HTTP ${res.status} from ${new URL(url).hostname}`);
93
+ return res.json();
94
+ }
95
+ // ── Schemas ───────────────────────────────────────────────────────────────────
96
+ const AddressSchema = zod_1.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "must be a valid 0x address");
97
+ const ScoreTokenSchema = zod_1.z.object({ address: AddressSchema, minLiquidity: zod_1.z.number().positive().optional() });
98
+ const CheckTokenSchema = zod_1.z.object({ address: AddressSchema });
99
+ const ScanDipsSchema = zod_1.z.object({
100
+ minScore: zod_1.z.number().min(0).max(100).optional(),
101
+ minLiquidity: zod_1.z.number().positive().optional(),
102
+ limit: zod_1.z.number().int().min(1).max(100).optional(),
103
+ }).default({});
104
+ // ── Tool definitions ──────────────────────────────────────────────────────────
105
+ exports.SCANNER_TOOLS = [
106
+ {
107
+ name: "score_token",
108
+ description: "Run the 6-component dip-reversal score on any Base token. Returns a 0–100 score, pattern label (DEEP-REVERSAL / REVERSAL / DIP-BUY / SHALLOW-DIP), and full component breakdown. Data from DexScreener — no API key required.",
109
+ inputSchema: {
110
+ type: "object",
111
+ properties: {
112
+ address: { type: "string", description: "Token contract address on Base (0x…)" },
113
+ minLiquidity: { type: "number", description: "Minimum liquidity in USD (default 50000)" },
114
+ },
115
+ required: ["address"],
116
+ },
117
+ },
118
+ {
119
+ name: "check_token",
120
+ description: "Security audit a Base token: honeypot, rug risk score, mint authority, freeze authority, LP lock %, buy/sell tax, holder count. Powered by GoPlusLabs (free). Always run this before buying an unknown token.",
121
+ inputSchema: {
122
+ type: "object",
123
+ properties: {
124
+ address: { type: "string", description: "Token contract address on Base (0x…)" },
125
+ },
126
+ required: ["address"],
127
+ },
128
+ },
129
+ {
130
+ name: "scan_dips",
131
+ description: "Scan all trending + new Base pools right now for dip-reversal opportunities. Fetches GeckoTerminal data, runs the 6-component scorer on each pool, returns ranked candidates. Zero API keys required.",
132
+ inputSchema: {
133
+ type: "object",
134
+ properties: {
135
+ minScore: { type: "number", description: "Min score to include in results (default 50)" },
136
+ minLiquidity: { type: "number", description: "Min pool liquidity in USD (default 50000)" },
137
+ limit: { type: "number", description: "Max pools to scan (default 40, max 100)" },
138
+ },
139
+ required: [],
140
+ },
141
+ },
142
+ ];
143
+ // ── Handlers ──────────────────────────────────────────────────────────────────
144
+ async function handleScannerTool(name, args) {
145
+ // ── score_token ────────────────────────────────────────────────────────────
146
+ if (name === "score_token") {
147
+ const parsed = ScoreTokenSchema.safeParse(args);
148
+ if (!parsed.success)
149
+ return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
150
+ const { address, minLiquidity = DEFAULT_MIN_LIQ } = parsed.data;
151
+ const data = await fetchJson(`https://api.dexscreener.com/latest/dex/tokens/${address}`);
152
+ const pair = (data.pairs ?? [])
153
+ .filter((p) => p.chainId === "base")
154
+ .sort((a, b) => (b.liquidity?.usd ?? 0) - (a.liquidity?.usd ?? 0))[0];
155
+ if (!pair) {
156
+ return { content: [{ type: "text", text: `No Base trading pair found for \`${address}\`. Check the address or try a different token.` }], isError: true };
157
+ }
158
+ const c = {
159
+ mint: address,
160
+ symbol: pair.baseToken?.symbol ?? address.slice(0, 8),
161
+ priceUsd: parseFloat(pair.priceUsd ?? "0"),
162
+ priceChange5m: pair.priceChange?.m5 ?? 0,
163
+ priceChange1h: pair.priceChange?.h1 ?? 0,
164
+ priceChange6h: pair.priceChange?.h6 ?? 0,
165
+ priceChange24h: pair.priceChange?.h24 ?? 0,
166
+ volume1h: pair.volume?.h1 ?? 0,
167
+ liquidity: pair.liquidity?.usd ?? 0,
168
+ buys5m: pair.txns?.m5?.buys ?? 0,
169
+ sells5m: pair.txns?.m5?.sells ?? 0,
170
+ buys1h: pair.txns?.h1?.buys ?? 0,
171
+ sells1h: pair.txns?.h1?.sells ?? 0,
172
+ };
173
+ const result = scoreDipReversal(c, minLiquidity);
174
+ const bd = result.breakdown;
175
+ const bar = "█".repeat(Math.round(result.score / 5)).padEnd(20, "░");
176
+ const lines = [
177
+ `## Dip-Reversal Score: ${c.symbol}`,
178
+ `\`${address}\``,
179
+ ``,
180
+ `**Score: ${result.score}/100** \`${bar}\``,
181
+ `**Pattern:** ${result.pattern ?? "—"}`,
182
+ `**Price:** $${c.priceUsd.toFixed(8).replace(/0+$/, "").replace(/\.$/, "")}`,
183
+ `**Liquidity:** $${(c.liquidity / 1000).toFixed(0)}k`,
184
+ ``,
185
+ ];
186
+ if (!result.passed) {
187
+ lines.push(`**Gates failed — not a valid dip-reversal setup:**`);
188
+ result.gateFailures.forEach(f => lines.push(`• ${f}`));
189
+ }
190
+ else {
191
+ lines.push(`**Breakdown:**`);
192
+ lines.push(`| Component | Signal | Points |`);
193
+ lines.push(`|-----------|--------|--------|`);
194
+ lines.push(`| Drop depth | ${bd.dropDepth?.value}% (1h) | ${bd.dropDepth?.points}/25 |`);
195
+ lines.push(`| Bounce | ${bd.bounce?.value}% (5m) | ${bd.bounce?.points}/20 |`);
196
+ lines.push(`| Sentiment shift | ${bd.sentimentShift?.value} ratio shift | ${bd.sentimentShift?.points}/15 |`);
197
+ lines.push(`| Buy pressure | ${bd.buyPressure?.value}% buyers (5m) | ${bd.buyPressure?.points}/10 |`);
198
+ lines.push(`| Activity | $${((bd.activity?.vol1h ?? 0) / 1000).toFixed(0)}k vol / ${bd.activity?.txns1h} txns | ${bd.activity?.points}/15 |`);
199
+ lines.push(`| Trend | 6h ${bd.trendAlignment?.pc6h}% / 24h ${bd.trendAlignment?.pc24h}% | ${(bd.trendAlignment?.points ?? 0) > 0 ? "+" : ""}${bd.trendAlignment?.points}/15 |`);
200
+ lines.push(``);
201
+ if (result.score >= 65)
202
+ lines.push(`**Strong setup.** Score ≥65 — high probability dip-reversal.`);
203
+ else if (result.score >= 50)
204
+ lines.push(`**Marginal.** Score 50–64 — look for additional confirmation before entry.`);
205
+ else
206
+ lines.push(`**Weak.** Score <50 — skip this setup.`);
207
+ lines.push(``, `Run \`check_token address="${address}"\` for a rug/security check before buying.`);
208
+ }
209
+ return { content: [{ type: "text", text: lines.join("\n") }] };
210
+ }
211
+ // ── check_token ────────────────────────────────────────────────────────────
212
+ if (name === "check_token") {
213
+ const parsed = CheckTokenSchema.safeParse(args);
214
+ if (!parsed.success)
215
+ return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
216
+ const { address } = parsed.data;
217
+ // GoPlusLabs — chain 8453 = Base mainnet
218
+ const data = await fetchJson(`https://api.gopluslabs.io/api/v1/token_security/8453?contract_addresses=${address}`);
219
+ const info = data.result?.[address.toLowerCase()] ?? data.result?.[address] ?? {};
220
+ const isHoneypot = info.is_honeypot === "1";
221
+ const isMintable = info.is_mintable === "1";
222
+ const isFreezeAuth = info.transfer_pausable === "1";
223
+ const isOpenSource = info.is_open_source === "1";
224
+ // LP lock: sum of all locked LP holders
225
+ const lpLockedPct = (info.lp_holders ?? [])
226
+ .filter(h => h.is_locked)
227
+ .reduce((s, h) => s + parseFloat(h.percent ?? "0"), 0) * 100;
228
+ const buyTax = parseFloat(info.buy_tax ?? "0");
229
+ const sellTax = parseFloat(info.sell_tax ?? "0");
230
+ // Rug score: accumulate risk factors
231
+ let rugScore = 0;
232
+ if (isHoneypot)
233
+ rugScore += 50; // cannot sell → auto-danger
234
+ if (isMintable)
235
+ rugScore += 30; // unlimited supply risk
236
+ if (isFreezeAuth)
237
+ rugScore += 30; // transfers can be blocked
238
+ if (lpLockedPct < 50)
239
+ rugScore += 20; // LP can be pulled
240
+ if (sellTax > 10)
241
+ rugScore += 15; // high sell tax
242
+ rugScore = Math.min(100, rugScore);
243
+ const verdict = (isHoneypot || rugScore >= 60) ? "DANGER"
244
+ : rugScore >= 30 ? "CAUTION"
245
+ : "SAFE";
246
+ const icon = { DANGER: "🔴", CAUTION: "🟡", SAFE: "🟢" }[verdict];
247
+ const lines = [
248
+ `## Token Security Check`,
249
+ `\`${address}\``,
250
+ ``,
251
+ `**Verdict: ${icon} ${verdict}** (rug score: ${rugScore}/100)`,
252
+ ``,
253
+ `| Check | Status |`,
254
+ `|-------|--------|`,
255
+ `| Honeypot | ${isHoneypot ? "🔴 YES — cannot sell" : "🟢 No"} |`,
256
+ `| Mint authority | ${isMintable ? "🔴 Yes — supply can inflate" : "🟢 No"} |`,
257
+ `| Transfer freeze | ${isFreezeAuth ? "🔴 Yes — transfers can be paused" : "🟢 No"} |`,
258
+ `| Open source | ${isOpenSource ? "🟢 Yes" : "🔴 No — unverified contract"} |`,
259
+ `| LP locked | ${lpLockedPct >= 80 ? "🟢" : lpLockedPct >= 50 ? "🟡" : "🔴"} ${lpLockedPct.toFixed(1)}% |`,
260
+ `| Buy tax | ${buyTax > 5 ? "🟡" : "🟢"} ${buyTax}% |`,
261
+ `| Sell tax | ${sellTax > 10 ? "🔴" : sellTax > 5 ? "🟡" : "🟢"} ${sellTax}% |`,
262
+ `| Holder count | ${info.holder_count ?? "unknown"} |`,
263
+ ``,
264
+ ];
265
+ if (verdict === "DANGER") {
266
+ lines.push(`**Do not buy.** This token has critical red flags. High risk of total loss.`);
267
+ }
268
+ else if (verdict === "CAUTION") {
269
+ lines.push(`**Trade carefully.** Elevated risk — verify team, community, and LP lock before buying. Keep position size small.`);
270
+ }
271
+ else {
272
+ lines.push(`**Passes basic security checks.** No critical red flags found. Always DYOR — security checks are not a guarantee.`);
273
+ }
274
+ return { content: [{ type: "text", text: lines.join("\n") }] };
275
+ }
276
+ // ── scan_dips ──────────────────────────────────────────────────────────────
277
+ if (name === "scan_dips") {
278
+ const parsed = ScanDipsSchema.safeParse(args ?? {});
279
+ if (!parsed.success)
280
+ return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
281
+ const { minScore = DEFAULT_MIN_SCORE, minLiquidity = DEFAULT_MIN_LIQ, limit = 40 } = parsed.data;
282
+ // Fetch trending + new pools on Base from GeckoTerminal in parallel
283
+ const [trendingRes, newPoolsRes] = await Promise.allSettled([
284
+ fetchJson("https://api.geckoterminal.com/api/v2/networks/base/trending_pools?page=1"),
285
+ fetchJson("https://api.geckoterminal.com/api/v2/networks/base/new_pools?page=1"),
286
+ ]);
287
+ const rawPools = [
288
+ ...(trendingRes.status === "fulfilled" ? trendingRes.value.data ?? [] : []),
289
+ ...(newPoolsRes.status === "fulfilled" ? newPoolsRes.value.data ?? [] : []),
290
+ ];
291
+ if (!rawPools.length) {
292
+ return { content: [{ type: "text", text: "GeckoTerminal returned no pools. Try again in a moment." }], isError: true };
293
+ }
294
+ // Deduplicate by pool id
295
+ const seen = new Set();
296
+ const pools = rawPools.filter(p => {
297
+ if (!p?.attributes || !p.id)
298
+ return false;
299
+ if (seen.has(p.id))
300
+ return false;
301
+ seen.add(p.id);
302
+ return true;
303
+ });
304
+ // Map GeckoTerminal pool → Candidate
305
+ const candidates = pools
306
+ .map((p) => {
307
+ const a = p.attributes ?? {};
308
+ const txns = a.transactions ?? {};
309
+ const pc = a.price_change_percentage ?? {};
310
+ const vol = a.volume_usd ?? {};
311
+ const liq = parseFloat(a.reserve_in_usd ?? "0");
312
+ const tokenRel = p.relationships?.base_token?.data?.id ?? "";
313
+ const mint = tokenRel.includes("_") ? tokenRel.split("_")[1] : tokenRel;
314
+ if (!mint?.startsWith("0x"))
315
+ return null;
316
+ return {
317
+ mint,
318
+ symbol: (a.name ?? "").split(" / ")[0] || mint.slice(0, 8),
319
+ priceUsd: parseFloat(a.base_token_price_usd ?? "0"),
320
+ priceChange5m: parseFloat(pc.m5 ?? "0"),
321
+ priceChange1h: parseFloat(pc.h1 ?? "0"),
322
+ priceChange6h: parseFloat(pc.h6 ?? "0"),
323
+ priceChange24h: parseFloat(pc.h24 ?? "0"),
324
+ volume1h: parseFloat(vol.h1 ?? "0"),
325
+ liquidity: liq,
326
+ buys5m: txns.m5?.buys ?? 0,
327
+ sells5m: txns.m5?.sells ?? 0,
328
+ buys1h: txns.h1?.buys ?? 0,
329
+ sells1h: txns.h1?.sells ?? 0,
330
+ };
331
+ })
332
+ .filter((c) => c !== null && c.liquidity >= minLiquidity)
333
+ .slice(0, limit);
334
+ const scored = candidates
335
+ .map(c => ({ ...c, ...scoreDipReversal(c, minLiquidity) }))
336
+ .filter(c => c.passed && c.score >= minScore)
337
+ .sort((a, b) => b.score - a.score);
338
+ if (!scored.length) {
339
+ return {
340
+ content: [{
341
+ type: "text",
342
+ text: [
343
+ `## Dip Scan — No Setups Found`,
344
+ ``,
345
+ `Scanned **${candidates.length} pools** on Base. None passed the dip-reversal gates with score ≥ ${minScore}.`,
346
+ ``,
347
+ `**This is a signal in itself** — when nothing scores, the market is likely not in dip-reversal mode.`,
348
+ ``,
349
+ `Try: \`scan_dips minScore=30\` to see weak setups, or check back in 5–10 minutes.`,
350
+ ].join("\n"),
351
+ }],
352
+ };
353
+ }
354
+ const lines = [
355
+ `## Dip Scan — ${scored.length} Setup${scored.length !== 1 ? "s" : ""} Found`,
356
+ `Scanned **${candidates.length} pools** · Score ≥ ${minScore} · Liq ≥ $${(minLiquidity / 1000).toFixed(0)}k`,
357
+ ``,
358
+ ];
359
+ for (const c of scored.slice(0, 10)) {
360
+ const bar = "█".repeat(Math.round(c.score / 10)).padEnd(10, "░");
361
+ const bd = c.breakdown;
362
+ const sign = (n) => n >= 0 ? `+${n.toFixed(1)}` : n.toFixed(1);
363
+ lines.push(`### ${c.symbol} · ${c.score}/100 \`${bar}\``);
364
+ lines.push(`**Pattern:** ${c.pattern} · **Liq:** $${(c.liquidity / 1000).toFixed(0)}k · **1h:** ${sign(c.priceChange1h)}% · **5m:** ${sign(c.priceChange5m)}%`);
365
+ lines.push(`**Buy pressure:** ${c.buyPressure5m.toFixed(0)}% · **Vol 1h:** $${((bd.activity?.vol1h ?? 0) / 1000).toFixed(0)}k · **Txns:** ${bd.activity?.txns1h ?? 0}`);
366
+ lines.push(`**Trend:** 6h ${sign(c.priceChange6h)}% / 24h ${sign(c.priceChange24h)}%`);
367
+ lines.push(`\`${c.mint}\``);
368
+ lines.push(``);
369
+ }
370
+ lines.push(`---`);
371
+ lines.push(`Next steps: \`score_token\` for full breakdown · \`check_token\` for rug check before buying`);
372
+ return { content: [{ type: "text", text: lines.join("\n") }] };
373
+ }
374
+ return null;
375
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noelclaw/mcp",
3
- "version": "1.5.6",
3
+ "version": "2.1.0",
4
4
  "description": "Noelclaw as an MCP skill — persistent memory, multi-agent coordination, scenario simulation, DeFi execution, and Sentinel-gated playbooks.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {