@noelclaw/mcp 2.3.1 → 2.4.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 +10 -9
- package/dist/server.js +38 -28
- package/dist/tools/automation.js +38 -0
- package/dist/tools/coder.js +62 -0
- package/dist/tools/defi.js +127 -0
- package/dist/tools/humanizer.js +143 -2
- package/dist/tools/insight.js +197 -19
- package/dist/tools/market.js +182 -7
- package/dist/tools/memory.js +159 -43
- package/dist/tools/miroshark.js +15 -4
- package/dist/tools/os.js +223 -0
- package/dist/tools/scanner.js +183 -52
- package/dist/tools/swarm.js +327 -14
- package/dist/tools/vault.js +89 -144
- package/package.json +5 -2
- package/dist/tools/news.js +0 -6
- package/dist/tools/research.js +0 -8
- package/dist/tools/twitter.js +0 -67
package/dist/tools/scanner.js
CHANGED
|
@@ -85,13 +85,116 @@ function scoreDipReversal(c, minLiquidity = DEFAULT_MIN_LIQ) {
|
|
|
85
85
|
buyPressure5m: bp,
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
|
-
// ── Fetch
|
|
88
|
+
// ── Fetch helpers ─────────────────────────────────────────────────────────────
|
|
89
89
|
async function fetchJson(url) {
|
|
90
90
|
const res = await fetch(url, { signal: AbortSignal.timeout(15000) });
|
|
91
91
|
if (!res.ok)
|
|
92
92
|
throw new Error(`HTTP ${res.status} from ${new URL(url).hostname}`);
|
|
93
93
|
return res.json();
|
|
94
94
|
}
|
|
95
|
+
async function fetchBasePools(minLiquidity, limit) {
|
|
96
|
+
const [trendingRes, newPoolsRes] = await Promise.allSettled([
|
|
97
|
+
fetchJson("https://api.geckoterminal.com/api/v2/networks/base/trending_pools?page=1"),
|
|
98
|
+
fetchJson("https://api.geckoterminal.com/api/v2/networks/base/new_pools?page=1"),
|
|
99
|
+
]);
|
|
100
|
+
const rawPools = [
|
|
101
|
+
...(trendingRes.status === "fulfilled" ? trendingRes.value.data ?? [] : []),
|
|
102
|
+
...(newPoolsRes.status === "fulfilled" ? newPoolsRes.value.data ?? [] : []),
|
|
103
|
+
];
|
|
104
|
+
if (!rawPools.length)
|
|
105
|
+
throw new Error("GeckoTerminal returned no pools. Try again in a moment.");
|
|
106
|
+
const seen = new Set();
|
|
107
|
+
const deduped = rawPools.filter(p => {
|
|
108
|
+
if (!p?.attributes || !p.id)
|
|
109
|
+
return false;
|
|
110
|
+
if (seen.has(p.id))
|
|
111
|
+
return false;
|
|
112
|
+
seen.add(p.id);
|
|
113
|
+
return true;
|
|
114
|
+
});
|
|
115
|
+
return deduped
|
|
116
|
+
.map((p) => {
|
|
117
|
+
const a = p.attributes ?? {};
|
|
118
|
+
const txns = a.transactions ?? {};
|
|
119
|
+
const pc = a.price_change_percentage ?? {};
|
|
120
|
+
const vol = a.volume_usd ?? {};
|
|
121
|
+
const liq = parseFloat(a.reserve_in_usd ?? "0");
|
|
122
|
+
const tokenRel = p.relationships?.base_token?.data?.id ?? "";
|
|
123
|
+
const mint = tokenRel.includes("_") ? tokenRel.split("_")[1] : tokenRel;
|
|
124
|
+
if (!mint?.startsWith("0x"))
|
|
125
|
+
return null;
|
|
126
|
+
return {
|
|
127
|
+
mint,
|
|
128
|
+
symbol: (a.name ?? "").split(" / ")[0] || mint.slice(0, 8),
|
|
129
|
+
priceUsd: parseFloat(a.base_token_price_usd ?? "0"),
|
|
130
|
+
priceChange5m: parseFloat(pc.m5 ?? "0"),
|
|
131
|
+
priceChange1h: parseFloat(pc.h1 ?? "0"),
|
|
132
|
+
priceChange6h: parseFloat(pc.h6 ?? "0"),
|
|
133
|
+
priceChange24h: parseFloat(pc.h24 ?? "0"),
|
|
134
|
+
volume1h: parseFloat(vol.h1 ?? "0"),
|
|
135
|
+
liquidity: liq,
|
|
136
|
+
buys5m: txns.m5?.buys ?? 0,
|
|
137
|
+
sells5m: txns.m5?.sells ?? 0,
|
|
138
|
+
buys1h: txns.h1?.buys ?? 0,
|
|
139
|
+
sells1h: txns.h1?.sells ?? 0,
|
|
140
|
+
};
|
|
141
|
+
})
|
|
142
|
+
.filter((c) => c !== null && c.liquidity >= minLiquidity)
|
|
143
|
+
.slice(0, limit);
|
|
144
|
+
}
|
|
145
|
+
function scoreMomentum(c, minLiquidity = DEFAULT_MIN_LIQ) {
|
|
146
|
+
const pc5m = c.priceChange5m;
|
|
147
|
+
const pc1h = c.priceChange1h;
|
|
148
|
+
const pc6h = c.priceChange6h;
|
|
149
|
+
const pc24h = c.priceChange24h;
|
|
150
|
+
const liq = c.liquidity;
|
|
151
|
+
const vol1h = c.volume1h;
|
|
152
|
+
const totalTxns5m = c.buys5m + c.sells5m;
|
|
153
|
+
const buyRatio5m = totalTxns5m > 0 ? c.buys5m / totalTxns5m : 0;
|
|
154
|
+
const totalTxns1h = c.buys1h + c.sells1h;
|
|
155
|
+
const buyRatio1h = totalTxns1h > 0 ? c.buys1h / totalTxns1h : 0;
|
|
156
|
+
const bp = buyRatio5m * 100;
|
|
157
|
+
// Hard gates — all must pass
|
|
158
|
+
const gateFailures = [];
|
|
159
|
+
if (pc1h < 3)
|
|
160
|
+
gateFailures.push(`1h momentum weak (${pc1h.toFixed(1)}% < 3%)`);
|
|
161
|
+
if (pc5m < 0.5)
|
|
162
|
+
gateFailures.push(`5m not accelerating (${pc5m.toFixed(1)}% < 0.5%)`);
|
|
163
|
+
if (totalTxns5m > 5 && buyRatio5m < 0.55)
|
|
164
|
+
gateFailures.push(`buy pressure low (${bp.toFixed(0)}% < 55%)`);
|
|
165
|
+
if (liq < minLiquidity)
|
|
166
|
+
gateFailures.push(`liquidity $${(liq / 1000).toFixed(0)}k < $${(minLiquidity / 1000).toFixed(0)}k min`);
|
|
167
|
+
if (pc24h > 150)
|
|
168
|
+
gateFailures.push(`already parabolic (24h ${pc24h.toFixed(0)}%)`);
|
|
169
|
+
if (gateFailures.length > 0) {
|
|
170
|
+
return { score: 0, passed: false, pattern: null, gateFailures, buyPressure5m: bp };
|
|
171
|
+
}
|
|
172
|
+
// 1. Momentum strength (0–25 pts)
|
|
173
|
+
const momentumPts = pc1h >= 20 ? 25 : pc1h >= 10 ? 20 : pc1h >= 6 ? 15 : pc1h >= 3 ? 8 : 3;
|
|
174
|
+
// 2. 5m acceleration (0–20 pts)
|
|
175
|
+
const accelPts = pc5m >= 5 ? 20 : pc5m >= 3 ? 16 : pc5m >= 2 ? 12 : pc5m >= 1 ? 7 : 3;
|
|
176
|
+
// 3. Buy pressure (0–15 pts)
|
|
177
|
+
const bpPts = bp >= 70 ? 15 : bp >= 65 ? 12 : bp >= 60 ? 9 : bp >= 55 ? 5 : 2;
|
|
178
|
+
// 4. Volume & activity (0–15 pts)
|
|
179
|
+
const actPts = vol1h >= 100000 && totalTxns1h >= 200 ? 15
|
|
180
|
+
: vol1h >= 50000 && totalTxns1h >= 100 ? 12
|
|
181
|
+
: vol1h >= 20000 && totalTxns1h >= 40 ? 8
|
|
182
|
+
: vol1h >= 5000 && totalTxns1h >= 10 ? 4 : 1;
|
|
183
|
+
// 5. Trend continuation (0–15 pts)
|
|
184
|
+
let trendPts = 0;
|
|
185
|
+
if (pc6h > 5 && pc24h > 5)
|
|
186
|
+
trendPts = 15;
|
|
187
|
+
else if (pc6h > 0 && pc24h > 0)
|
|
188
|
+
trendPts = 10;
|
|
189
|
+
else if (pc6h > 0)
|
|
190
|
+
trendPts = 5;
|
|
191
|
+
// 6. Sentiment acceleration (0–10 pts)
|
|
192
|
+
const sentAccel = buyRatio5m - buyRatio1h;
|
|
193
|
+
const sentPts = sentAccel >= 0.10 ? 10 : sentAccel >= 0.05 ? 7 : sentAccel >= 0 ? 3 : 0;
|
|
194
|
+
const score = Math.max(0, Math.min(100, momentumPts + accelPts + bpPts + actPts + trendPts + sentPts));
|
|
195
|
+
const pattern = pc1h >= 15 ? "BREAKOUT" : pc1h >= 8 ? "MOMENTUM" : pc1h >= 3 ? "PUSH" : "WEAK-PUSH";
|
|
196
|
+
return { score, passed: true, pattern, gateFailures: [], buyPressure5m: bp };
|
|
197
|
+
}
|
|
95
198
|
// ── Schemas ───────────────────────────────────────────────────────────────────
|
|
96
199
|
const AddressSchema = zod_1.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "must be a valid 0x address");
|
|
97
200
|
const ScoreTokenSchema = zod_1.z.object({ address: AddressSchema, minLiquidity: zod_1.z.number().positive().optional() });
|
|
@@ -101,6 +204,11 @@ const ScanDipsSchema = zod_1.z.object({
|
|
|
101
204
|
minLiquidity: zod_1.z.number().positive().optional(),
|
|
102
205
|
limit: zod_1.z.number().int().min(1).max(100).optional(),
|
|
103
206
|
}).default({});
|
|
207
|
+
const ScanMomentumSchema = zod_1.z.object({
|
|
208
|
+
minScore: zod_1.z.number().min(0).max(100).optional(),
|
|
209
|
+
minLiquidity: zod_1.z.number().positive().optional(),
|
|
210
|
+
limit: zod_1.z.number().int().min(1).max(100).optional(),
|
|
211
|
+
}).default({});
|
|
104
212
|
// ── Tool definitions ──────────────────────────────────────────────────────────
|
|
105
213
|
exports.SCANNER_TOOLS = [
|
|
106
214
|
{
|
|
@@ -139,6 +247,22 @@ exports.SCANNER_TOOLS = [
|
|
|
139
247
|
required: [],
|
|
140
248
|
},
|
|
141
249
|
},
|
|
250
|
+
{
|
|
251
|
+
name: "scan_momentum",
|
|
252
|
+
description: "Scan Base pools for momentum breakout setups — tokens with strong 1h+ upward momentum that are still accelerating. " +
|
|
253
|
+
"The inverse of scan_dips: finds BREAKOUT / MOMENTUM / PUSH patterns instead of dip-reversals. " +
|
|
254
|
+
"Gates: 1h > +3%, 5m still rising, buy pressure > 55%, not already parabolic (24h < 150%). " +
|
|
255
|
+
"Returns scored and ranked candidates. No API keys required.",
|
|
256
|
+
inputSchema: {
|
|
257
|
+
type: "object",
|
|
258
|
+
properties: {
|
|
259
|
+
minScore: { type: "number", description: "Min score to include (default 50)" },
|
|
260
|
+
minLiquidity: { type: "number", description: "Min pool liquidity in USD (default 50000)" },
|
|
261
|
+
limit: { type: "number", description: "Max pools to scan (default 40, max 100)" },
|
|
262
|
+
},
|
|
263
|
+
required: [],
|
|
264
|
+
},
|
|
265
|
+
},
|
|
142
266
|
];
|
|
143
267
|
// ── Handlers ──────────────────────────────────────────────────────────────────
|
|
144
268
|
async function handleScannerTool(name, args) {
|
|
@@ -279,58 +403,13 @@ async function handleScannerTool(name, args) {
|
|
|
279
403
|
if (!parsed.success)
|
|
280
404
|
return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
|
|
281
405
|
const { minScore = DEFAULT_MIN_SCORE, minLiquidity = DEFAULT_MIN_LIQ, limit = 40 } = parsed.data;
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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 };
|
|
406
|
+
let candidates;
|
|
407
|
+
try {
|
|
408
|
+
candidates = await fetchBasePools(minLiquidity, limit);
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
return { content: [{ type: "text", text: err.message }], isError: true };
|
|
293
412
|
}
|
|
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
413
|
const scored = candidates
|
|
335
414
|
.map(c => ({ ...c, ...scoreDipReversal(c, minLiquidity) }))
|
|
336
415
|
.filter(c => c.passed && c.score >= minScore)
|
|
@@ -371,5 +450,57 @@ async function handleScannerTool(name, args) {
|
|
|
371
450
|
lines.push(`Next steps: \`score_token\` for full breakdown · \`check_token\` for rug check before buying`);
|
|
372
451
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
373
452
|
}
|
|
453
|
+
// ── scan_momentum ──────────────────────────────────────────────────────────
|
|
454
|
+
if (name === "scan_momentum") {
|
|
455
|
+
const parsed = ScanMomentumSchema.safeParse(args ?? {});
|
|
456
|
+
if (!parsed.success)
|
|
457
|
+
return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
|
|
458
|
+
const { minScore = DEFAULT_MIN_SCORE, minLiquidity = DEFAULT_MIN_LIQ, limit = 40 } = parsed.data;
|
|
459
|
+
let candidates;
|
|
460
|
+
try {
|
|
461
|
+
candidates = await fetchBasePools(minLiquidity, limit);
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
return { content: [{ type: "text", text: err.message }], isError: true };
|
|
465
|
+
}
|
|
466
|
+
const scored = candidates
|
|
467
|
+
.map(c => ({ ...c, ...scoreMomentum(c, minLiquidity) }))
|
|
468
|
+
.filter(c => c.passed && c.score >= minScore)
|
|
469
|
+
.sort((a, b) => b.score - a.score);
|
|
470
|
+
if (!scored.length) {
|
|
471
|
+
return {
|
|
472
|
+
content: [{
|
|
473
|
+
type: "text",
|
|
474
|
+
text: [
|
|
475
|
+
`## Momentum Scan — No Breakouts Found`,
|
|
476
|
+
``,
|
|
477
|
+
`Scanned **${candidates.length} pools** on Base. None passed the momentum gates with score ≥ ${minScore}.`,
|
|
478
|
+
``,
|
|
479
|
+
`When nothing breaks out, the market may be in consolidation or distribution.`,
|
|
480
|
+
``,
|
|
481
|
+
`Try: \`scan_dips\` to look for reversal opportunities instead.`,
|
|
482
|
+
].join("\n"),
|
|
483
|
+
}],
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
const sign = (n) => n >= 0 ? `+${n.toFixed(1)}` : n.toFixed(1);
|
|
487
|
+
const lines = [
|
|
488
|
+
`## Momentum Scan — ${scored.length} Breakout${scored.length !== 1 ? "s" : ""} Found`,
|
|
489
|
+
`Scanned **${candidates.length} pools** · Score ≥ ${minScore} · Liq ≥ $${(minLiquidity / 1000).toFixed(0)}k`,
|
|
490
|
+
``,
|
|
491
|
+
];
|
|
492
|
+
for (const c of scored.slice(0, 10)) {
|
|
493
|
+
const bar = "█".repeat(Math.round(c.score / 10)).padEnd(10, "░");
|
|
494
|
+
lines.push(`### ${c.symbol} · ${c.score}/100 \`${bar}\``);
|
|
495
|
+
lines.push(`**Pattern:** ${c.pattern} · **Liq:** $${(c.liquidity / 1000).toFixed(0)}k · **1h:** ${sign(c.priceChange1h)}% · **5m:** ${sign(c.priceChange5m)}%`);
|
|
496
|
+
lines.push(`**Buy pressure:** ${c.buyPressure5m.toFixed(0)}% · **Vol 1h:** $${(c.volume1h / 1000).toFixed(0)}k`);
|
|
497
|
+
lines.push(`**Trend:** 6h ${sign(c.priceChange6h)}% / 24h ${sign(c.priceChange24h)}%`);
|
|
498
|
+
lines.push(`\`${c.mint}\``);
|
|
499
|
+
lines.push(``);
|
|
500
|
+
}
|
|
501
|
+
lines.push(`---`);
|
|
502
|
+
lines.push(`Next steps: \`score_token\` for dip-reversal score · \`check_token\` for rug check before buying`);
|
|
503
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
504
|
+
}
|
|
374
505
|
return null;
|
|
375
506
|
}
|