@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/insight.js
CHANGED
|
@@ -5,6 +5,7 @@ exports.handleInsightTool = handleInsightTool;
|
|
|
5
5
|
const zod_1 = require("zod");
|
|
6
6
|
const convex_js_1 = require("../convex.js");
|
|
7
7
|
const llm_js_1 = require("../llm.js");
|
|
8
|
+
const memory_js_1 = require("./memory.js");
|
|
8
9
|
exports.INSIGHT_TOOLS = [
|
|
9
10
|
{
|
|
10
11
|
name: "ask_noel",
|
|
@@ -26,35 +27,212 @@ exports.INSIGHT_TOOLS = [
|
|
|
26
27
|
required: ["question"],
|
|
27
28
|
},
|
|
28
29
|
},
|
|
30
|
+
{
|
|
31
|
+
name: "market_thesis",
|
|
32
|
+
description: "Generate a structured bull vs bear thesis for any token or market topic. " +
|
|
33
|
+
"Noel fetches live price/market data then writes a sharp, two-sided analysis: " +
|
|
34
|
+
"bull case (catalysts, narratives, technicals), bear case (risks, headwinds, red flags), " +
|
|
35
|
+
"and a net verdict with conviction score 0–10. " +
|
|
36
|
+
"Use before opening a position or for research on a token you're watching.",
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
token: { type: "string", description: "Token symbol or CoinGecko ID, e.g. 'ETH', 'bitcoin', 'AERO'" },
|
|
41
|
+
context: { type: "string", description: "Optional: extra context — your time horizon, thesis seed, or specific concerns" },
|
|
42
|
+
},
|
|
43
|
+
required: ["token"],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "trade_plan",
|
|
48
|
+
description: "Build a structured trade plan for any token: entry zone, stop loss, take profit levels, " +
|
|
49
|
+
"position size recommendation (as % of portfolio), and risk/reward ratio. " +
|
|
50
|
+
"Based on live price data + Noel's market reading. Returns a ready-to-act plan, not vague advice.",
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {
|
|
54
|
+
token: { type: "string", description: "Token symbol or CoinGecko ID" },
|
|
55
|
+
side: { type: "string", enum: ["long", "short"], description: "Trade direction (default: long)" },
|
|
56
|
+
portfolioSize: { type: "number", description: "Optional: your portfolio size in USD (for position sizing)" },
|
|
57
|
+
riskTolerance: { type: "string", enum: ["conservative", "moderate", "aggressive"], description: "Risk profile (default: moderate)" },
|
|
58
|
+
timeframe: { type: "string", description: "Optional: trade timeframe, e.g. 'intraday', 'swing', 'weeks'" },
|
|
59
|
+
},
|
|
60
|
+
required: ["token"],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
29
63
|
];
|
|
30
64
|
const AskNoelSchema = zod_1.z.object({
|
|
31
65
|
question: zod_1.z.string().min(1),
|
|
32
66
|
messages: zod_1.z.array(zod_1.z.object({ role: zod_1.z.enum(["user", "assistant"]), content: zod_1.z.string() })).optional(),
|
|
33
67
|
});
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
68
|
+
const MarketThesisSchema = zod_1.z.object({
|
|
69
|
+
token: zod_1.z.string().min(1),
|
|
70
|
+
context: zod_1.z.string().optional(),
|
|
71
|
+
});
|
|
72
|
+
const TradePlanSchema = zod_1.z.object({
|
|
73
|
+
token: zod_1.z.string().min(1),
|
|
74
|
+
side: zod_1.z.enum(["long", "short"]).optional(),
|
|
75
|
+
portfolioSize: zod_1.z.number().positive().optional(),
|
|
76
|
+
riskTolerance: zod_1.z.enum(["conservative", "moderate", "aggressive"]).optional(),
|
|
77
|
+
timeframe: zod_1.z.string().optional(),
|
|
78
|
+
});
|
|
79
|
+
const NOEL_BASE_PROMPT = `You are Noel, a crypto AI analyst and the core intelligence of the Noelclaw AI Operating System. You have 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.`;
|
|
80
|
+
async function buildSystemPrompt(question) {
|
|
81
|
+
const memories = await (0, memory_js_1.searchSupermemory)(question, 5);
|
|
82
|
+
if (!memories.length)
|
|
83
|
+
return NOEL_BASE_PROMPT;
|
|
84
|
+
const memBlock = memories
|
|
85
|
+
.map(r => {
|
|
86
|
+
const title = r.metadata?.title ? `[${r.metadata.title}] ` : "";
|
|
87
|
+
return `- ${title}${r.content.slice(0, 250).replace(/\n/g, " ")}`;
|
|
88
|
+
})
|
|
89
|
+
.join("\n");
|
|
90
|
+
return `${NOEL_BASE_PROMPT}\n\n<user_memory>\nThe following is stored knowledge about this user — use it to personalize your response:\n${memBlock}\n</user_memory>`;
|
|
91
|
+
}
|
|
92
|
+
async function fetchCgPrice(token) {
|
|
93
|
+
try {
|
|
94
|
+
const id = token.toLowerCase().replace(/ /g, "-");
|
|
95
|
+
const res = await fetch(`https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=${id}&order=market_cap_desc&per_page=1&page=1`, { signal: AbortSignal.timeout(8000) });
|
|
96
|
+
if (!res.ok)
|
|
97
|
+
return null;
|
|
98
|
+
const data = await res.json();
|
|
99
|
+
const coin = data[0];
|
|
100
|
+
if (!coin)
|
|
101
|
+
return null;
|
|
102
|
+
return {
|
|
103
|
+
price: coin.current_price ?? 0,
|
|
104
|
+
change24h: coin.price_change_percentage_24h ?? 0,
|
|
105
|
+
mcap: coin.market_cap ?? 0,
|
|
106
|
+
symbol: coin.symbol?.toUpperCase() ?? token.toUpperCase(),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
37
110
|
return null;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function handleInsightTool(name, args) {
|
|
114
|
+
if (name === "ask_noel") {
|
|
115
|
+
const parsed = AskNoelSchema.safeParse(args);
|
|
116
|
+
if (!parsed.success)
|
|
117
|
+
return { content: [{ type: "text", text: `Invalid input: question ${parsed.error.issues[0].message}` }], isError: true };
|
|
118
|
+
const { question, messages = [] } = parsed.data;
|
|
119
|
+
const systemPrompt = await buildSystemPrompt(question);
|
|
120
|
+
if (process.env.ANTHROPIC_API_KEY || process.env.BANKR_API_KEY) {
|
|
121
|
+
try {
|
|
122
|
+
const history = messages.map(m => ({ role: m.role, content: m.content }));
|
|
123
|
+
const answer = await (0, llm_js_1.callLLM)(systemPrompt, question, 1024, history);
|
|
124
|
+
return { content: [{ type: "text", text: answer }] };
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
// fall through to Convex
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const data = await (0, convex_js_1.callConvex)("/mcp/chat", "POST", {
|
|
131
|
+
question,
|
|
132
|
+
agentId: "noel-default",
|
|
133
|
+
messages,
|
|
134
|
+
systemPrompt,
|
|
135
|
+
}, "ask_noel");
|
|
136
|
+
return { content: [{ type: "text", text: data.answer ?? JSON.stringify(data) }] };
|
|
137
|
+
}
|
|
138
|
+
if (name === "market_thesis") {
|
|
139
|
+
const parsed = MarketThesisSchema.safeParse(args);
|
|
140
|
+
if (!parsed.success)
|
|
141
|
+
return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
|
|
142
|
+
const { token, context } = parsed.data;
|
|
143
|
+
const priceData = await fetchCgPrice(token);
|
|
144
|
+
const dataCtx = priceData
|
|
145
|
+
? `Current price: $${priceData.price.toLocaleString()} | 24h: ${priceData.change24h.toFixed(1)}% | Mcap: $${(priceData.mcap / 1000000).toFixed(0)}M`
|
|
146
|
+
: `(live price data unavailable — use general knowledge)`;
|
|
147
|
+
const prompt = [
|
|
148
|
+
`Write a structured bull vs bear thesis for ${token.toUpperCase()}.`,
|
|
149
|
+
``,
|
|
150
|
+
`Live market data: ${dataCtx}`,
|
|
151
|
+
context ? `User context: ${context}` : "",
|
|
152
|
+
``,
|
|
153
|
+
`Output format (use exactly these headers):`,
|
|
154
|
+
`## ${token.toUpperCase()} Thesis`,
|
|
155
|
+
`**Current price:** [price] | **24h:** [change]`,
|
|
156
|
+
``,
|
|
157
|
+
`### Bull Case`,
|
|
158
|
+
`(3-5 specific catalysts, narratives, or technical factors that support upside. Be concrete — no vague "adoption" claims.)`,
|
|
159
|
+
``,
|
|
160
|
+
`### Bear Case`,
|
|
161
|
+
`(3-5 specific risks, headwinds, or red flags. Include on-chain, macro, and competitive risks if applicable.)`,
|
|
162
|
+
``,
|
|
163
|
+
`### Net Verdict`,
|
|
164
|
+
`**Conviction:** X/10`,
|
|
165
|
+
`(2-3 sentences: the deciding factor, the key risk to watch, and your net lean.)`,
|
|
166
|
+
].filter(Boolean).join("\n");
|
|
167
|
+
try {
|
|
168
|
+
const systemPrompt = await buildSystemPrompt(`${token} ${context ?? ""}`);
|
|
169
|
+
const answer = await (0, llm_js_1.callLLM)(systemPrompt, prompt, 1200);
|
|
170
|
+
return { content: [{ type: "text", text: answer }] };
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
return { content: [{ type: "text", text: `market_thesis error: ${err.message}` }], isError: true };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (name === "trade_plan") {
|
|
177
|
+
const parsed = TradePlanSchema.safeParse(args);
|
|
178
|
+
if (!parsed.success)
|
|
179
|
+
return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
|
|
180
|
+
const { token, side = "long", portfolioSize, riskTolerance = "moderate", timeframe } = parsed.data;
|
|
181
|
+
const priceData = await fetchCgPrice(token);
|
|
182
|
+
const dataCtx = priceData
|
|
183
|
+
? `Current price: $${priceData.price.toLocaleString()} | 24h: ${priceData.change24h.toFixed(1)}% | Mcap: $${(priceData.mcap / 1000000).toFixed(0)}M`
|
|
184
|
+
: `(live price data unavailable)`;
|
|
185
|
+
const riskPcts = {
|
|
186
|
+
conservative: "1-2% of portfolio",
|
|
187
|
+
moderate: "2-5% of portfolio",
|
|
188
|
+
aggressive: "5-10% of portfolio",
|
|
189
|
+
};
|
|
190
|
+
const prompt = [
|
|
191
|
+
`Build a structured ${side.toUpperCase()} trade plan for ${token.toUpperCase()}.`,
|
|
192
|
+
``,
|
|
193
|
+
`Live data: ${dataCtx}`,
|
|
194
|
+
`Risk tolerance: ${riskTolerance} (max position size: ${riskPcts[riskTolerance]})`,
|
|
195
|
+
portfolioSize ? `Portfolio size: $${portfolioSize.toLocaleString()}` : "",
|
|
196
|
+
timeframe ? `Timeframe: ${timeframe}` : "",
|
|
197
|
+
``,
|
|
198
|
+
`Output exactly this format:`,
|
|
199
|
+
`## Trade Plan: ${token.toUpperCase()} ${side.toUpperCase()}`,
|
|
200
|
+
``,
|
|
201
|
+
`**Direction:** ${side.toUpperCase()}`,
|
|
202
|
+
`**Timeframe:** [fill]`,
|
|
203
|
+
``,
|
|
204
|
+
`### Entry`,
|
|
205
|
+
`- Ideal entry: $[price or range]`,
|
|
206
|
+
`- Entry condition: [what must happen / trigger]`,
|
|
207
|
+
``,
|
|
208
|
+
`### Risk Management`,
|
|
209
|
+
`- Stop loss: $[price] ([%] below/above entry)`,
|
|
210
|
+
`- Position size: [% of portfolio]${portfolioSize ? ` = $[USD amount]` : ""}`,
|
|
211
|
+
`- Max loss: [% of portfolio]`,
|
|
212
|
+
``,
|
|
213
|
+
`### Targets`,
|
|
214
|
+
`- TP1: $[price] ([%] gain) — partial exit [%]`,
|
|
215
|
+
`- TP2: $[price] ([%] gain) — partial exit [%]`,
|
|
216
|
+
`- TP3: $[price] ([%] gain) — final exit`,
|
|
217
|
+
``,
|
|
218
|
+
`### Risk/Reward`,
|
|
219
|
+
`- RR ratio: [X:1]`,
|
|
220
|
+
`- Expected value: [+/- %]`,
|
|
221
|
+
``,
|
|
222
|
+
`### Thesis in One Sentence`,
|
|
223
|
+
`[Why this trade makes sense right now]`,
|
|
224
|
+
``,
|
|
225
|
+
`### Invalidation`,
|
|
226
|
+
`[Exactly what would make you exit early / kill the thesis]`,
|
|
227
|
+
].filter(Boolean).join("\n");
|
|
44
228
|
try {
|
|
45
|
-
const
|
|
46
|
-
const answer = await (0, llm_js_1.callLLM)(
|
|
229
|
+
const systemPrompt = await buildSystemPrompt(`${token} trade ${riskTolerance} ${timeframe ?? ""}`);
|
|
230
|
+
const answer = await (0, llm_js_1.callLLM)(systemPrompt, prompt, 1200);
|
|
47
231
|
return { content: [{ type: "text", text: answer }] };
|
|
48
232
|
}
|
|
49
233
|
catch (err) {
|
|
50
|
-
|
|
234
|
+
return { content: [{ type: "text", text: `trade_plan error: ${err.message}` }], isError: true };
|
|
51
235
|
}
|
|
52
236
|
}
|
|
53
|
-
|
|
54
|
-
const data = await (0, convex_js_1.callConvex)("/mcp/chat", "POST", {
|
|
55
|
-
question,
|
|
56
|
-
agentId: "noel-default",
|
|
57
|
-
messages,
|
|
58
|
-
}, "ask_noel");
|
|
59
|
-
return { content: [{ type: "text", text: data.answer ?? JSON.stringify(data) }] };
|
|
237
|
+
return null;
|
|
60
238
|
}
|
package/dist/tools/market.js
CHANGED
|
@@ -23,6 +23,20 @@ async function cgFetch(path) {
|
|
|
23
23
|
throw new Error(`CoinGecko ${res.status}`);
|
|
24
24
|
return res.json();
|
|
25
25
|
}
|
|
26
|
+
async function resolveTokenId(query) {
|
|
27
|
+
const upper = query.trim().toUpperCase();
|
|
28
|
+
if (SYMBOL_TO_ID[upper])
|
|
29
|
+
return { id: SYMBOL_TO_ID[upper], symbol: upper };
|
|
30
|
+
// Fallback: search CoinGecko — handles any token not in the static map
|
|
31
|
+
try {
|
|
32
|
+
const res = await cgFetch(`/search?query=${encodeURIComponent(query)}`);
|
|
33
|
+
const coin = res.coins?.[0];
|
|
34
|
+
if (coin?.id)
|
|
35
|
+
return { id: coin.id, symbol: coin.symbol?.toUpperCase() ?? upper };
|
|
36
|
+
}
|
|
37
|
+
catch { }
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
26
40
|
function fmt(n, decimals = 2) {
|
|
27
41
|
if (n == null)
|
|
28
42
|
return "—";
|
|
@@ -63,9 +77,55 @@ exports.MARKET_TOOLS = [
|
|
|
63
77
|
required: ["question"],
|
|
64
78
|
},
|
|
65
79
|
},
|
|
80
|
+
{
|
|
81
|
+
name: "compare_tokens",
|
|
82
|
+
description: "Compare 2–5 tokens side by side — price, 24h/7d change, market cap, volume, and ATH drawdown. " +
|
|
83
|
+
"Ideal for deciding between assets or tracking a portfolio watchlist.",
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: {
|
|
87
|
+
tokens: {
|
|
88
|
+
type: "array",
|
|
89
|
+
items: { type: "string" },
|
|
90
|
+
description: "2–5 token symbols to compare, e.g. ['BTC', 'ETH', 'SOL']",
|
|
91
|
+
minItems: 2,
|
|
92
|
+
maxItems: 5,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
required: ["tokens"],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "market_overview",
|
|
100
|
+
description: "Global crypto market snapshot: Fear & Greed Index, BTC dominance, total market cap, DeFi TVL, " +
|
|
101
|
+
"ETH gas, trending tokens, and top sector leaders. Use for a full market briefing.",
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: "object",
|
|
104
|
+
properties: {},
|
|
105
|
+
required: [],
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: "token_history",
|
|
110
|
+
description: "Get historical price data for a token. Returns OHLC candles for the requested timeframe. " +
|
|
111
|
+
"Use to understand price trends, identify support/resistance levels, or calculate % changes over time.",
|
|
112
|
+
inputSchema: {
|
|
113
|
+
type: "object",
|
|
114
|
+
properties: {
|
|
115
|
+
token: { type: "string", description: "Token symbol, e.g. 'BTC', 'ETH', 'SOL'" },
|
|
116
|
+
days: {
|
|
117
|
+
type: "number",
|
|
118
|
+
description: "Number of days of history (1=24h, 7=7d, 30=30d, 90=90d, 365=1y). Default: 7",
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
required: ["token"],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
66
124
|
];
|
|
67
125
|
const GetMarketDataSchema = zod_1.z.object({ token: zod_1.z.string().optional() });
|
|
68
126
|
const GetTokenDataSchema = zod_1.z.object({ question: zod_1.z.string().min(1) });
|
|
127
|
+
const CompareTokensSchema = zod_1.z.object({ tokens: zod_1.z.array(zod_1.z.string()).min(2).max(5) });
|
|
128
|
+
const TokenHistorySchema = zod_1.z.object({ token: zod_1.z.string().min(1), days: zod_1.z.number().positive().optional() });
|
|
69
129
|
async function fetchMarketSnapshot() {
|
|
70
130
|
try {
|
|
71
131
|
const data = await cgFetch("/coins/markets?vs_currency=usd&ids=bitcoin,ethereum,solana&sparkline=false&price_change_percentage=24h");
|
|
@@ -91,10 +151,10 @@ async function handleMarketTool(name, args) {
|
|
|
91
151
|
return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
|
|
92
152
|
const { token } = parsed.data;
|
|
93
153
|
if (token) {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
154
|
+
const resolved = await resolveTokenId(token);
|
|
155
|
+
if (!resolved)
|
|
156
|
+
return { content: [{ type: "text", text: `Token not found: "${token}". Try a full name like "pepe" or a known symbol.` }], isError: true };
|
|
157
|
+
const { id, symbol: sym } = resolved;
|
|
98
158
|
const [data, trending] = await Promise.all([
|
|
99
159
|
cgFetch(`/coins/markets?vs_currency=usd&ids=${id}&sparkline=false&price_change_percentage=24h`),
|
|
100
160
|
cgFetch("/search/trending"),
|
|
@@ -148,9 +208,14 @@ async function handleMarketTool(name, args) {
|
|
|
148
208
|
const parsed = GetTokenDataSchema.safeParse(args);
|
|
149
209
|
if (!parsed.success)
|
|
150
210
|
return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
|
|
151
|
-
const q = parsed.data.question
|
|
152
|
-
|
|
153
|
-
const
|
|
211
|
+
const q = parsed.data.question;
|
|
212
|
+
// Try to extract a known symbol first, then fall back to search
|
|
213
|
+
const upperQ = q.toUpperCase();
|
|
214
|
+
const knownSym = Object.keys(SYMBOL_TO_ID).find((s) => new RegExp(`\\b${s}\\b`).test(upperQ));
|
|
215
|
+
const resolved = await resolveTokenId(knownSym ?? q);
|
|
216
|
+
if (!resolved)
|
|
217
|
+
return { content: [{ type: "text", text: `Token not found: "${q}". Try a symbol like "ETH" or a full name.` }], isError: true };
|
|
218
|
+
const { id, symbol: sym } = resolved;
|
|
154
219
|
const data = await cgFetch(`/coins/markets?vs_currency=usd&ids=${id}&sparkline=false&price_change_percentage=24h`);
|
|
155
220
|
const c = data[0];
|
|
156
221
|
if (!c)
|
|
@@ -168,6 +233,116 @@ async function handleMarketTool(name, args) {
|
|
|
168
233
|
];
|
|
169
234
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
170
235
|
}
|
|
236
|
+
case "compare_tokens": {
|
|
237
|
+
const parsed = CompareTokensSchema.safeParse(args);
|
|
238
|
+
if (!parsed.success)
|
|
239
|
+
return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
|
|
240
|
+
const syms = parsed.data.tokens.map(t => t.toUpperCase());
|
|
241
|
+
const ids = syms.map(s => SYMBOL_TO_ID[s]).filter(Boolean);
|
|
242
|
+
const unknown = syms.filter(s => !SYMBOL_TO_ID[s]);
|
|
243
|
+
if (!ids.length)
|
|
244
|
+
return { content: [{ type: "text", text: `Unknown tokens: ${unknown.join(", ")}` }], isError: true };
|
|
245
|
+
const data = await cgFetch(`/coins/markets?vs_currency=usd&ids=${ids.join(",")}&sparkline=false&price_change_percentage=24h,7d`);
|
|
246
|
+
const header = [
|
|
247
|
+
`**Token Comparison** — ${new Date().toUTCString()}`,
|
|
248
|
+
unknown.length ? `\n⚠️ Unknown: ${unknown.join(", ")}` : "",
|
|
249
|
+
``,
|
|
250
|
+
`| Token | Price | 24h | 7d | Mcap | Vol 24h | ATH% |`,
|
|
251
|
+
`|-------|-------|-----|----|------|---------|------|`,
|
|
252
|
+
].filter(Boolean);
|
|
253
|
+
const rows = data.map((c) => {
|
|
254
|
+
const sym = c.symbol?.toUpperCase();
|
|
255
|
+
const ch24 = c.price_change_percentage_24h ?? 0;
|
|
256
|
+
const ch7d = c.price_change_percentage_7d_in_currency ?? 0;
|
|
257
|
+
const athPct = c.ath_change_percentage ?? 0;
|
|
258
|
+
const s = (n) => `${n >= 0 ? "+" : ""}${fmt(n)}%`;
|
|
259
|
+
return `| **${sym}** | ${fmtPrice(c.current_price)} | ${s(ch24)} | ${s(ch7d)} | ${fmtB(c.market_cap)} | ${fmtB(c.total_volume)} | ${fmt(athPct)}% |`;
|
|
260
|
+
});
|
|
261
|
+
return { content: [{ type: "text", text: [...header, ...rows].join("\n") }] };
|
|
262
|
+
}
|
|
263
|
+
case "market_overview": {
|
|
264
|
+
const [globalData, fearGreed, trending] = await Promise.allSettled([
|
|
265
|
+
cgFetch("/global"),
|
|
266
|
+
fetch("https://api.alternative.me/fng/", { signal: AbortSignal.timeout(8000) }).then(r => r.json()),
|
|
267
|
+
cgFetch("/search/trending"),
|
|
268
|
+
]);
|
|
269
|
+
const global = globalData.status === "fulfilled" ? globalData.value.data : null;
|
|
270
|
+
const fg = fearGreed.status === "fulfilled" ? fearGreed.value?.data?.[0] : null;
|
|
271
|
+
const trendCoins = trending.status === "fulfilled" ? (trending.value?.coins ?? []) : [];
|
|
272
|
+
const totalMcap = global?.total_market_cap?.usd;
|
|
273
|
+
const defiTvl = global?.total_value_locked?.usd;
|
|
274
|
+
const btcDom = global?.market_cap_percentage?.btc;
|
|
275
|
+
const ethDom = global?.market_cap_percentage?.eth;
|
|
276
|
+
const mcap24hChange = global?.market_cap_change_percentage_24h_usd;
|
|
277
|
+
const fgLabel = fg ? `${fg.value}/100 — ${fg.value_classification}` : "unavailable";
|
|
278
|
+
const fgEmoji = fg ? (Number(fg.value) >= 75 ? "🟢 Extreme Greed" : Number(fg.value) >= 55 ? "🟢 Greed" : Number(fg.value) >= 45 ? "🟡 Neutral" : Number(fg.value) >= 25 ? "🔴 Fear" : "🔴 Extreme Fear") : "";
|
|
279
|
+
const lines = [
|
|
280
|
+
`## 🌍 Global Crypto Market`,
|
|
281
|
+
`_${new Date().toUTCString()}_`,
|
|
282
|
+
``,
|
|
283
|
+
`**Fear & Greed:** ${fgEmoji} ${fg?.value ?? "—"}/100 (${fg?.value_classification ?? "—"})`,
|
|
284
|
+
totalMcap ? `**Total Market Cap:** ${fmtB(totalMcap)} (${mcap24hChange != null ? `${mcap24hChange >= 0 ? "+" : ""}${fmt(mcap24hChange)}% 24h` : ""})` : "",
|
|
285
|
+
btcDom != null ? `**BTC Dominance:** ${fmt(btcDom)}% | **ETH:** ${fmt(ethDom ?? 0)}%` : "",
|
|
286
|
+
defiTvl ? `**DeFi TVL:** ${fmtB(defiTvl)}` : "",
|
|
287
|
+
global?.active_cryptocurrencies ? `**Active Coins:** ${global.active_cryptocurrencies.toLocaleString()}` : "",
|
|
288
|
+
``,
|
|
289
|
+
].filter(l => l !== "");
|
|
290
|
+
if (trendCoins.length > 0) {
|
|
291
|
+
lines.push(`**🔥 Trending Now**`);
|
|
292
|
+
for (const t of trendCoins.slice(0, 7)) {
|
|
293
|
+
const item = t.item;
|
|
294
|
+
const rank = item.market_cap_rank ? `#${item.market_cap_rank}` : "unranked";
|
|
295
|
+
lines.push(`• **${item.symbol}** (${rank}) — ${item.name}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
299
|
+
}
|
|
300
|
+
case "token_history": {
|
|
301
|
+
const parsed = TokenHistorySchema.safeParse(args);
|
|
302
|
+
if (!parsed.success)
|
|
303
|
+
return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
|
|
304
|
+
const days = parsed.data.days ?? 7;
|
|
305
|
+
const resolved = await resolveTokenId(parsed.data.token);
|
|
306
|
+
if (!resolved)
|
|
307
|
+
return { content: [{ type: "text", text: `Token not found: "${parsed.data.token}". Try a symbol like "ETH" or a full name.` }], isError: true };
|
|
308
|
+
const { id, symbol: sym } = resolved;
|
|
309
|
+
const [ohlc, current] = await Promise.all([
|
|
310
|
+
cgFetch(`/coins/${id}/ohlc?vs_currency=usd&days=${days}`),
|
|
311
|
+
cgFetch(`/coins/markets?vs_currency=usd&ids=${id}&sparkline=false&price_change_percentage=24h`),
|
|
312
|
+
]);
|
|
313
|
+
const c = current[0];
|
|
314
|
+
const candles = ohlc ?? [];
|
|
315
|
+
if (!candles.length)
|
|
316
|
+
return { content: [{ type: "text", text: `No history data for ${sym}` }], isError: true };
|
|
317
|
+
const first = candles[0];
|
|
318
|
+
const last = candles[candles.length - 1];
|
|
319
|
+
const openPrice = first[1];
|
|
320
|
+
const closePrice = last[4];
|
|
321
|
+
const periodChange = ((closePrice - openPrice) / openPrice) * 100;
|
|
322
|
+
const highs = candles.map(c => c[2]);
|
|
323
|
+
const lows = candles.map(c => c[3]);
|
|
324
|
+
const periodHigh = Math.max(...highs);
|
|
325
|
+
const periodLow = Math.min(...lows);
|
|
326
|
+
const lines = [
|
|
327
|
+
`## ${sym} — ${days}d History`,
|
|
328
|
+
``,
|
|
329
|
+
`**Current:** ${fmtPrice(c?.current_price)} (${(c?.price_change_percentage_24h ?? 0) >= 0 ? "+" : ""}${fmt(c?.price_change_percentage_24h)}% 24h)`,
|
|
330
|
+
`**Period open:** ${fmtPrice(openPrice)}`,
|
|
331
|
+
`**Period close:** ${fmtPrice(closePrice)} (${periodChange >= 0 ? "+" : ""}${fmt(periodChange)}% over ${days}d)`,
|
|
332
|
+
`**${days}d High:** ${fmtPrice(periodHigh)}`,
|
|
333
|
+
`**${days}d Low:** ${fmtPrice(periodLow)}`,
|
|
334
|
+
`**Range:** ${fmt((periodHigh - periodLow) / periodLow * 100)}% spread`,
|
|
335
|
+
``,
|
|
336
|
+
`**Last 10 candles (OHLC):**`,
|
|
337
|
+
`| Date | Open | High | Low | Close |`,
|
|
338
|
+
`|------|------|------|-----|-------|`,
|
|
339
|
+
...candles.slice(-10).map(([ts, o, h, l, cl]) => {
|
|
340
|
+
const d = new Date(ts).toISOString().slice(0, 10);
|
|
341
|
+
return `| ${d} | ${fmtPrice(o)} | ${fmtPrice(h)} | ${fmtPrice(l)} | ${fmtPrice(cl)} |`;
|
|
342
|
+
}),
|
|
343
|
+
];
|
|
344
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
345
|
+
}
|
|
171
346
|
default:
|
|
172
347
|
return null;
|
|
173
348
|
}
|