mcp-server-madeonsol 1.2.0 → 1.3.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.d.ts +2 -0
- package/dist/index.js +763 -0
- package/package.json +1 -1
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { createServer } from "node:http";
|
|
7
|
+
const BASE_URL = process.env.MADEONSOL_API_URL || "https://madeonsol.com";
|
|
8
|
+
const MADEONSOL_API_KEY = process.env.MADEONSOL_API_KEY; // Native key from madeonsol.com/developer
|
|
9
|
+
const PRIVATE_KEY = process.env.SVM_PRIVATE_KEY; // x402 micropayments (for AI agents)
|
|
10
|
+
const PORT = parseInt(process.env.PORT || "3100", 10);
|
|
11
|
+
const MODE = process.env.MCP_TRANSPORT || "stdio"; // "stdio" or "http"
|
|
12
|
+
let authMode = "none";
|
|
13
|
+
let paidFetch = fetch;
|
|
14
|
+
function apiKeyHeaders() {
|
|
15
|
+
if (authMode === "madeonsol") {
|
|
16
|
+
return { Authorization: `Bearer ${MADEONSOL_API_KEY}` };
|
|
17
|
+
}
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
async function initAuth() {
|
|
21
|
+
if (MADEONSOL_API_KEY) {
|
|
22
|
+
authMode = "madeonsol";
|
|
23
|
+
console.error("[madeonsol-mcp] Using MadeOnSol API key (Bearer auth)");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (PRIVATE_KEY) {
|
|
27
|
+
try {
|
|
28
|
+
const { wrapFetchWithPayment } = await import("@x402/fetch");
|
|
29
|
+
const { x402Client } = await import("@x402/core/client");
|
|
30
|
+
const { ExactSvmScheme } = await import("@x402/svm/exact/client");
|
|
31
|
+
const { createKeyPairSignerFromBytes } = await import("@solana/kit");
|
|
32
|
+
const { base58 } = await import("@scure/base");
|
|
33
|
+
const signer = await createKeyPairSignerFromBytes(base58.decode(PRIVATE_KEY));
|
|
34
|
+
const client = new x402Client();
|
|
35
|
+
client.register("solana:*", new ExactSvmScheme(signer));
|
|
36
|
+
paidFetch = wrapFetchWithPayment(fetch, client);
|
|
37
|
+
authMode = "x402";
|
|
38
|
+
console.error(`[madeonsol-mcp] x402 payments enabled, wallet: ${signer.address}`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
console.error("[madeonsol-mcp] x402 setup failed:", err);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
console.error("\n[madeonsol-mcp] No auth configured — every tool call will fail.\n" +
|
|
46
|
+
" → Get a free MADEONSOL_API_KEY (200 req/day, no card) at https://madeonsol.com/developer\n" +
|
|
47
|
+
" → Or set SVM_PRIVATE_KEY for x402 micropayments.\n");
|
|
48
|
+
}
|
|
49
|
+
async function query(path, params) {
|
|
50
|
+
// API key uses /api/v1/ endpoints; x402 uses /api/x402/
|
|
51
|
+
const apiPath = authMode === "x402" || authMode === "none"
|
|
52
|
+
? path
|
|
53
|
+
: path.replace("/api/x402/", "/api/v1/");
|
|
54
|
+
const url = new URL(apiPath, BASE_URL);
|
|
55
|
+
if (params) {
|
|
56
|
+
for (const [k, v] of Object.entries(params)) {
|
|
57
|
+
if (v !== undefined)
|
|
58
|
+
url.searchParams.set(k, String(v));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const headers = apiKeyHeaders();
|
|
62
|
+
const res = authMode === "x402"
|
|
63
|
+
? await paidFetch(url.toString())
|
|
64
|
+
: await fetch(url.toString(), { headers });
|
|
65
|
+
if (!res.ok) {
|
|
66
|
+
const body = await res.text().catch(() => "");
|
|
67
|
+
return `Error ${res.status}: ${body}`;
|
|
68
|
+
}
|
|
69
|
+
return JSON.stringify(await res.json(), null, 2);
|
|
70
|
+
}
|
|
71
|
+
function registerTools(server) {
|
|
72
|
+
const readOnlyAnnotations = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true };
|
|
73
|
+
server.tool("madeonsol_kol_feed", "Get real-time Solana KOL trades from 1,000+ tracked wallets. PRO+ adds size/age/strategy/winrate filters.", {
|
|
74
|
+
limit: z.number().min(1).max(100).default(10).describe("Number of trades to return (1-100)"),
|
|
75
|
+
before: z.string().optional().describe("Cursor — ISO 8601 timestamp; returns trades strictly older than this. Pass next_before from the previous response for polling."),
|
|
76
|
+
action: z.enum(["buy", "sell"]).optional().describe("Filter by trade type: buy or sell"),
|
|
77
|
+
kol: z.string().optional().describe("Filter by specific KOL wallet address (base58)"),
|
|
78
|
+
min_sol: z.number().optional().describe("PRO+: minimum SOL size per trade"),
|
|
79
|
+
token_age_max_min: z.number().optional().describe("PRO+: max token age in minutes at time of trade"),
|
|
80
|
+
exclude_sells: z.boolean().optional().describe("PRO+: drop sell-side trades"),
|
|
81
|
+
min_kol_winrate: z.number().optional().describe("PRO+: minimum 7d winrate of the KOL (0-100)"),
|
|
82
|
+
strategy: z.enum(["scalper", "day_trader", "swing_trader", "hodler", "mixed"]).optional().describe("PRO+: filter by auto-tagged strategy"),
|
|
83
|
+
}, readOnlyAnnotations, async ({ limit, before, action, kol, min_sol, token_age_max_min, exclude_sells, min_kol_winrate, strategy }) => {
|
|
84
|
+
const params = { limit };
|
|
85
|
+
if (before)
|
|
86
|
+
params.before = before;
|
|
87
|
+
if (action)
|
|
88
|
+
params.action = action;
|
|
89
|
+
if (kol)
|
|
90
|
+
params.kol = kol;
|
|
91
|
+
if (min_sol !== undefined)
|
|
92
|
+
params.min_sol = min_sol;
|
|
93
|
+
if (token_age_max_min !== undefined)
|
|
94
|
+
params.token_age_max_min = token_age_max_min;
|
|
95
|
+
if (exclude_sells)
|
|
96
|
+
params.exclude_sells = "true";
|
|
97
|
+
if (min_kol_winrate !== undefined)
|
|
98
|
+
params.min_kol_winrate = min_kol_winrate;
|
|
99
|
+
if (strategy)
|
|
100
|
+
params.strategy = strategy;
|
|
101
|
+
return { content: [{ type: "text", text: await query("/api/x402/kol/feed", params) }] };
|
|
102
|
+
});
|
|
103
|
+
server.tool("madeonsol_kol_coordination", "KOL convergence signals (v1.1) — tokens being accumulated by multiple KOLs. Response includes peak_kols/peak_buys (busiest window slice), exited_count (net-flow-negative wallets), and a 0-100 coordination_score. Blacklist filters WIF/BONK/stables by default.", {
|
|
104
|
+
period: z.enum(["1h", "6h", "24h", "7d"]).default("24h").describe("Time period for coordination analysis"),
|
|
105
|
+
min_kols: z.number().min(2).max(50).default(3).describe("Minimum number of KOLs converging on the same token"),
|
|
106
|
+
limit: z.number().min(1).max(50).default(20).describe("Number of coordination signals to return"),
|
|
107
|
+
min_avg_winrate: z.number().optional().describe("PRO+: require cluster avg winrate_7d >= N (0-100)"),
|
|
108
|
+
unique_strategies: z.number().optional().describe("PRO+: require >= N distinct strategies in cluster"),
|
|
109
|
+
include_majors: z.boolean().optional().describe("v1.1: include major memecoins (WIF/BONK/POPCAT). Default false."),
|
|
110
|
+
window_minutes: z.number().min(1).max(60).optional().describe("v1.1: peak-density window (1-60). Default 15."),
|
|
111
|
+
min_score: z.number().min(0).max(100).optional().describe("v1.1: minimum composite coordination_score (0-100)."),
|
|
112
|
+
}, readOnlyAnnotations, async ({ period, min_kols, limit, min_avg_winrate, unique_strategies, include_majors, window_minutes, min_score }) => {
|
|
113
|
+
const params = { period, min_kols, limit };
|
|
114
|
+
if (min_avg_winrate !== undefined)
|
|
115
|
+
params.min_avg_winrate = min_avg_winrate;
|
|
116
|
+
if (unique_strategies !== undefined)
|
|
117
|
+
params.unique_strategies = unique_strategies;
|
|
118
|
+
if (include_majors !== undefined)
|
|
119
|
+
params.include_majors = include_majors ? "true" : "false";
|
|
120
|
+
if (window_minutes !== undefined)
|
|
121
|
+
params.window_minutes = window_minutes;
|
|
122
|
+
if (min_score !== undefined)
|
|
123
|
+
params.min_score = min_score;
|
|
124
|
+
return { content: [{ type: "text", text: await query("/api/x402/kol/coordination", params) }] };
|
|
125
|
+
});
|
|
126
|
+
server.tool("madeonsol_kol_leaderboard", "Get KOL performance rankings by PnL and win rate. PRO+ can sort by alternative axes (winrate/roi/profit_factor/early_entry).", {
|
|
127
|
+
period: z.enum(["today", "7d", "30d", "90d", "180d"]).default("7d").describe("Time period (trade retention is 180d)"),
|
|
128
|
+
limit: z.number().min(1).max(50).default(20).describe("Number of KOLs to return in ranking"),
|
|
129
|
+
sort: z.enum(["pnl", "winrate", "profit_factor", "roi", "early_entry"]).optional().describe("PRO+: sort axis (default 'pnl')"),
|
|
130
|
+
strategy: z.enum(["sniper", "flipper", "swinger", "holder", "mixed"]).optional().describe("PRO+: filter by strategy tag"),
|
|
131
|
+
min_winrate: z.number().optional().describe("PRO+: minimum winrate cutoff (0-100)"),
|
|
132
|
+
}, readOnlyAnnotations, async ({ period, limit, sort, strategy, min_winrate }) => {
|
|
133
|
+
const params = { period, limit };
|
|
134
|
+
if (sort)
|
|
135
|
+
params.sort = sort;
|
|
136
|
+
if (strategy)
|
|
137
|
+
params.strategy = strategy;
|
|
138
|
+
if (min_winrate !== undefined)
|
|
139
|
+
params.min_winrate = min_winrate;
|
|
140
|
+
return { content: [{ type: "text", text: await query("/api/x402/kol/leaderboard", params) }] };
|
|
141
|
+
});
|
|
142
|
+
server.tool("madeonsol_deployer_alerts", "Get real-time alerts from Pump.fun deployers with KOL buy enrichment. Filters: deployer tier, alert_type, priority, and min_kol_buys to gate out noise. Cursor-paginated via 'before' (preferred over 'offset' at scale).", {
|
|
143
|
+
limit: z.number().min(1).max(100).default(10).describe("Number of deployer alerts to return (1-100)"),
|
|
144
|
+
offset: z.number().min(0).default(0).describe("Legacy offset pagination (prefer 'before' for polling)"),
|
|
145
|
+
before: z.string().optional().describe("Cursor — ISO 8601 timestamp; returns alerts strictly older than this. Pass next_before from the previous response."),
|
|
146
|
+
since: z.string().optional().describe("Only alerts after this ISO 8601 timestamp."),
|
|
147
|
+
tier: z.enum(["elite", "good", "moderate", "rising", "cold"]).optional().describe("Filter by deployer tier. PRO/ULTRA only — BASIC callers receive HTTP 403."),
|
|
148
|
+
alert_type: z.string().optional().describe("Filter by alert_type (e.g. 'new_deploy', 'bonded')."),
|
|
149
|
+
priority: z.enum(["high", "medium", "low"]).optional().describe("Filter by alert priority."),
|
|
150
|
+
min_kol_buys: z.number().min(1).max(100).optional().describe("Only alerts where at least N KOLs bought the token (1-100)."),
|
|
151
|
+
}, readOnlyAnnotations, async ({ limit, offset, before, since, tier, alert_type, priority, min_kol_buys }) => {
|
|
152
|
+
const params = { limit, offset };
|
|
153
|
+
if (before)
|
|
154
|
+
params.before = before;
|
|
155
|
+
if (since)
|
|
156
|
+
params.since = since;
|
|
157
|
+
if (tier)
|
|
158
|
+
params.tier = tier;
|
|
159
|
+
if (alert_type)
|
|
160
|
+
params.alert_type = alert_type;
|
|
161
|
+
if (priority)
|
|
162
|
+
params.priority = priority;
|
|
163
|
+
if (min_kol_buys !== undefined)
|
|
164
|
+
params.min_kol_buys = min_kol_buys;
|
|
165
|
+
return { content: [{ type: "text", text: await query("/api/x402/deployer-hunter/alerts", params) }] };
|
|
166
|
+
});
|
|
167
|
+
server.tool("madeonsol_kol_pairs", "KOL affinity matrix — discover which KOLs frequently co-trade the same tokens within a time window.", {
|
|
168
|
+
period: z.enum(["7d", "30d"]).default("7d").describe("Time period: 7d or 30d"),
|
|
169
|
+
min_shared: z.number().min(1).max(20).default(3).describe("Minimum number of shared tokens to qualify as a pair"),
|
|
170
|
+
limit: z.number().min(1).max(50).default(20).describe("Number of KOL pairs to return"),
|
|
171
|
+
}, readOnlyAnnotations, async ({ period, min_shared, limit }) => ({
|
|
172
|
+
content: [{ type: "text", text: await query("/api/x402/kol/pairs", { period, min_shared, limit }) }],
|
|
173
|
+
}));
|
|
174
|
+
server.tool("madeonsol_kol_timing", "KOL entry/exit timing profile — hold duration, exit speed, and activity patterns for a specific KOL.", {
|
|
175
|
+
wallet: z.string().describe("KOL wallet address (base58)"),
|
|
176
|
+
period: z.enum(["7d", "30d"]).default("30d").describe("Time period: 7d or 30d"),
|
|
177
|
+
}, readOnlyAnnotations, async ({ wallet, period }) => {
|
|
178
|
+
if (authMode === "madeonsol") {
|
|
179
|
+
const headers = { ...apiKeyHeaders() };
|
|
180
|
+
const res = await fetch(`${BASE_URL}/api/v1/kol/${wallet}/timing?period=${period}`, { headers });
|
|
181
|
+
if (!res.ok) {
|
|
182
|
+
const body = await res.text().catch(() => "");
|
|
183
|
+
return { content: [{ type: "text", text: `Error ${res.status}: ${body}` }] };
|
|
184
|
+
}
|
|
185
|
+
return { content: [{ type: "text", text: JSON.stringify(await res.json(), null, 2) }] };
|
|
186
|
+
}
|
|
187
|
+
return { content: [{ type: "text", text: "KOL timing requires MADEONSOL_API_KEY (msk_) — get one free at madeonsol.com/developer." }] };
|
|
188
|
+
});
|
|
189
|
+
server.tool("madeonsol_deployer_trajectory", "Deployer skill curve — streaks, rolling bond rate, improvement trend, and deployment cadence for a Pump.fun deployer.", {
|
|
190
|
+
wallet: z.string().describe("Deployer wallet address (base58)"),
|
|
191
|
+
}, readOnlyAnnotations, async ({ wallet }) => {
|
|
192
|
+
if (authMode === "madeonsol") {
|
|
193
|
+
const headers = { ...apiKeyHeaders() };
|
|
194
|
+
const res = await fetch(`${BASE_URL}/api/v1/deployer-hunter/${wallet}/trajectory`, { headers });
|
|
195
|
+
if (!res.ok) {
|
|
196
|
+
const body = await res.text().catch(() => "");
|
|
197
|
+
return { content: [{ type: "text", text: `Error ${res.status}: ${body}` }] };
|
|
198
|
+
}
|
|
199
|
+
return { content: [{ type: "text", text: JSON.stringify(await res.json(), null, 2) }] };
|
|
200
|
+
}
|
|
201
|
+
return { content: [{ type: "text", text: "Deployer trajectory requires MADEONSOL_API_KEY (msk_, Pro/Ultra) — get one at madeonsol.com/developer." }] };
|
|
202
|
+
});
|
|
203
|
+
server.tool("madeonsol_kol_hot_tokens", "KOL momentum tokens — tokens with accelerating KOL buy interest, early signals before coordination triggers. PRO+ adds buyer-quality filters.", {
|
|
204
|
+
period: z.enum(["1h", "6h"]).default("6h").describe("Time period: 1h or 6h"),
|
|
205
|
+
min_kols: z.number().min(1).max(20).default(1).describe("Minimum KOL buyers to include a token"),
|
|
206
|
+
limit: z.number().min(1).max(50).default(20).describe("Number of hot tokens to return"),
|
|
207
|
+
min_avg_winrate: z.number().optional().describe("PRO+: require avg winrate_7d of buyers >= N (0-100)"),
|
|
208
|
+
unique_strategies: z.number().optional().describe("PRO+: require >= N distinct strategies among buyers"),
|
|
209
|
+
}, readOnlyAnnotations, async ({ period, min_kols, limit, min_avg_winrate, unique_strategies }) => {
|
|
210
|
+
const params = { period, min_kols, limit };
|
|
211
|
+
if (min_avg_winrate !== undefined)
|
|
212
|
+
params.min_avg_winrate = min_avg_winrate;
|
|
213
|
+
if (unique_strategies !== undefined)
|
|
214
|
+
params.unique_strategies = unique_strategies;
|
|
215
|
+
return { content: [{ type: "text", text: await query("/api/x402/kol/tokens/hot", params) }] };
|
|
216
|
+
});
|
|
217
|
+
server.tool("madeonsol_kol_token_entry_order", "Ranked KOL first-buyers for a specific token, ordered by entry timestamp. PRO+ adds percentile_pnl_7d per entry.", {
|
|
218
|
+
mint: z.string().describe("Token mint address (base58)"),
|
|
219
|
+
limit: z.number().min(1).max(200).default(50).describe("Max ranked entries to return"),
|
|
220
|
+
}, readOnlyAnnotations, async ({ mint, limit }) => ({
|
|
221
|
+
content: [{ type: "text", text: await query(`/api/x402/kol/tokens/${encodeURIComponent(mint)}/entry-order`, { limit }) }],
|
|
222
|
+
}));
|
|
223
|
+
server.tool("madeonsol_kol_compare_wallets", "Side-by-side comparison of 2-5 KOL wallets — strategy, winrates, ROI, percentile. PRO+ adds 30d overlap tokens (bought by 2+ of the wallets).", {
|
|
224
|
+
wallets: z.array(z.string()).min(2).max(5).describe("2-5 wallet addresses. BASIC=2, PRO=4, ULTRA=5."),
|
|
225
|
+
}, readOnlyAnnotations, async ({ wallets }) => ({
|
|
226
|
+
content: [{ type: "text", text: await query("/api/x402/kol/compare", { wallets: wallets.join(",") }) }],
|
|
227
|
+
}));
|
|
228
|
+
server.tool("madeonsol_kol_alerts_recent", "Live KOL alert feed — consensus clusters, fresh-token KOL buys, and heating-up wallets in one unified stream.", {
|
|
229
|
+
window: z.enum(["5m", "15m", "1h", "6h", "24h"]).default("15m").describe("Lookback window"),
|
|
230
|
+
types: z.array(z.enum(["consensus_cluster", "fresh_token_kol_buy", "heating_up"])).optional().describe("Filter to specific alert types"),
|
|
231
|
+
min_severity: z.enum(["low", "medium", "high"]).optional().describe("Minimum severity to include"),
|
|
232
|
+
limit: z.number().min(1).max(200).default(50).describe("Max alerts to return"),
|
|
233
|
+
}, readOnlyAnnotations, async ({ window, types, min_severity, limit }) => {
|
|
234
|
+
const params = { window, limit };
|
|
235
|
+
if (types && types.length > 0)
|
|
236
|
+
params.types = types.join(",");
|
|
237
|
+
if (min_severity)
|
|
238
|
+
params.min_severity = min_severity;
|
|
239
|
+
return { content: [{ type: "text", text: await query("/api/x402/kol/alerts/recent", params) }] };
|
|
240
|
+
});
|
|
241
|
+
server.tool("madeonsol_kol_pnl", "Deep per-wallet PnL breakdown — realized PnL, win rate, profit factor, max drawdown, daily equity curve, closed/open positions. BASIC: summary only. PRO: + curve + closed. ULTRA: + open positions.", {
|
|
242
|
+
wallet: z.string().describe("KOL wallet address (base58)"),
|
|
243
|
+
period: z.enum(["7d", "30d", "90d", "180d"]).default("30d").describe("Time period for PnL calculation"),
|
|
244
|
+
}, readOnlyAnnotations, async ({ wallet, period }) => {
|
|
245
|
+
if (authMode === "madeonsol") {
|
|
246
|
+
const headers = { ...apiKeyHeaders() };
|
|
247
|
+
const res = await fetch(`${BASE_URL}/api/v1/kol/${wallet}/pnl?period=${period}`, { headers });
|
|
248
|
+
if (!res.ok) {
|
|
249
|
+
const body = await res.text().catch(() => "");
|
|
250
|
+
return { content: [{ type: "text", text: `Error ${res.status}: ${body}` }] };
|
|
251
|
+
}
|
|
252
|
+
return { content: [{ type: "text", text: JSON.stringify(await res.json(), null, 2) }] };
|
|
253
|
+
}
|
|
254
|
+
return { content: [{ type: "text", text: "KOL PnL requires MADEONSOL_API_KEY (msk_) — get one free at madeonsol.com/developer." }] };
|
|
255
|
+
});
|
|
256
|
+
server.tool("madeonsol_kol_trending_tokens", "Tokens ranked by KOL buy volume — pure capital-flow signal. Sub-hour periods (5m/15m/30m) require PRO/ULTRA.", {
|
|
257
|
+
period: z.enum(["5m", "15m", "30m", "1h", "2h", "4h", "12h"]).default("1h").describe("Time window"),
|
|
258
|
+
min_kols: z.number().min(1).max(20).default(1).describe("Minimum KOL buyers"),
|
|
259
|
+
limit: z.number().min(1).max(50).default(20).describe("Number of trending tokens to return"),
|
|
260
|
+
}, readOnlyAnnotations, async ({ period, min_kols, limit }) => ({
|
|
261
|
+
content: [{ type: "text", text: await query("/api/x402/kol/tokens/trending", { period, min_kols, limit }) }],
|
|
262
|
+
}));
|
|
263
|
+
server.tool("madeonsol_discovery", "List all available MadeOnSol API endpoints with prices and parameter docs. Free, no auth required.", {}, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async () => {
|
|
264
|
+
const res = await fetch(new URL("/api/x402", BASE_URL).toString());
|
|
265
|
+
const data = await res.json();
|
|
266
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
267
|
+
});
|
|
268
|
+
// ── Wallet Tracker tools (REST auth only — all mutating operations) ──
|
|
269
|
+
{
|
|
270
|
+
const hasRestAuth = authMode === "madeonsol";
|
|
271
|
+
async function walletTrackerRequest(method, path, body) {
|
|
272
|
+
const headers = { "Content-Type": "application/json", ...apiKeyHeaders() };
|
|
273
|
+
const res = await fetch(`${BASE_URL}/api/v1${path}`, {
|
|
274
|
+
method,
|
|
275
|
+
headers,
|
|
276
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
277
|
+
});
|
|
278
|
+
if (!res.ok) {
|
|
279
|
+
const text = await res.text().catch(() => "");
|
|
280
|
+
return `Error ${res.status}: ${text}`;
|
|
281
|
+
}
|
|
282
|
+
return JSON.stringify(await res.json(), null, 2);
|
|
283
|
+
}
|
|
284
|
+
if (hasRestAuth) {
|
|
285
|
+
server.tool("madeonsol_wallet_tracker_watchlist", "List your tracked wallets with labels and remaining watchlist capacity. BASIC=10, PRO=50, ULTRA=100.", {}, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async () => ({
|
|
286
|
+
content: [{ type: "text", text: await walletTrackerRequest("GET", "/wallet-tracker/watchlist") }],
|
|
287
|
+
}));
|
|
288
|
+
server.tool("madeonsol_wallet_tracker_add", "Add a Solana wallet to your watchlist. Returns 409 if already tracked or limit reached.", {
|
|
289
|
+
wallet_address: z.string().describe("Solana wallet address (base58) to track"),
|
|
290
|
+
label: z.string().optional().describe("Optional human-readable label for this wallet"),
|
|
291
|
+
}, { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async ({ wallet_address, label }) => {
|
|
292
|
+
const body = { wallet_address };
|
|
293
|
+
if (label)
|
|
294
|
+
body.label = label;
|
|
295
|
+
return { content: [{ type: "text", text: await walletTrackerRequest("POST", "/wallet-tracker/watchlist", body) }] };
|
|
296
|
+
});
|
|
297
|
+
server.tool("madeonsol_wallet_tracker_remove", "Remove a wallet from your watchlist.", {
|
|
298
|
+
wallet_address: z.string().describe("Solana wallet address to remove from watchlist"),
|
|
299
|
+
}, { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }, async ({ wallet_address }) => ({
|
|
300
|
+
content: [{ type: "text", text: await walletTrackerRequest("DELETE", `/wallet-tracker/watchlist/${encodeURIComponent(wallet_address)}`) }],
|
|
301
|
+
}));
|
|
302
|
+
server.tool("madeonsol_wallet_tracker_trades", "Historical swap and transfer events for all your watched wallets. BASIC: truncated wallets, no tx_signature.", {
|
|
303
|
+
wallet: z.string().optional().describe("Filter to a specific wallet address"),
|
|
304
|
+
action: z.enum(["buy", "sell", "transfer_in", "transfer_out"]).optional().describe("Filter by action type"),
|
|
305
|
+
event_type: z.enum(["swap", "transfer"]).optional().describe("Filter by event type: swap (token trade) or transfer (SOL moved)"),
|
|
306
|
+
limit: z.number().min(1).max(200).default(50).describe("Max results (1–200)"),
|
|
307
|
+
before: z.number().optional().describe("Pagination cursor: block_time of the last event from previous page"),
|
|
308
|
+
}, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ wallet, action, event_type, limit, before }) => {
|
|
309
|
+
const params = { limit };
|
|
310
|
+
if (wallet)
|
|
311
|
+
params.wallet = wallet;
|
|
312
|
+
if (action)
|
|
313
|
+
params.action = action;
|
|
314
|
+
if (event_type)
|
|
315
|
+
params.event_type = event_type;
|
|
316
|
+
if (before !== undefined)
|
|
317
|
+
params.before = before;
|
|
318
|
+
const url = new URL(`${BASE_URL}/api/v1/wallet-tracker/trades`);
|
|
319
|
+
for (const [k, v] of Object.entries(params))
|
|
320
|
+
url.searchParams.set(k, String(v));
|
|
321
|
+
const res = await fetch(url.toString(), { headers: { "Content-Type": "application/json", ...apiKeyHeaders() } });
|
|
322
|
+
const text = res.ok ? JSON.stringify(await res.json(), null, 2) : `Error ${res.status}: ${await res.text().catch(() => "")}`;
|
|
323
|
+
return { content: [{ type: "text", text }] };
|
|
324
|
+
});
|
|
325
|
+
server.tool("madeonsol_wallet_tracker_summary", "Per-wallet stats: swap counts, SOL bought/sold, and last activity time across your watchlist.", {
|
|
326
|
+
period: z.enum(["24h", "7d", "30d"]).default("7d").describe("Time window for stats"),
|
|
327
|
+
wallet: z.string().optional().describe("Filter to a specific wallet address"),
|
|
328
|
+
}, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ period, wallet }) => {
|
|
329
|
+
const url = new URL(`${BASE_URL}/api/v1/wallet-tracker/summary`);
|
|
330
|
+
url.searchParams.set("period", period);
|
|
331
|
+
if (wallet)
|
|
332
|
+
url.searchParams.set("wallet", wallet);
|
|
333
|
+
const res = await fetch(url.toString(), { headers: { "Content-Type": "application/json", ...apiKeyHeaders() } });
|
|
334
|
+
const text = res.ok ? JSON.stringify(await res.json(), null, 2) : `Error ${res.status}: ${await res.text().catch(() => "")}`;
|
|
335
|
+
return { content: [{ type: "text", text }] };
|
|
336
|
+
});
|
|
337
|
+
console.error("[madeonsol-mcp] Wallet tracker tools enabled");
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
console.error("[madeonsol-mcp] Wallet tracker tools disabled (requires MADEONSOL_API_KEY)");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// ── Webhook & Streaming tools (require MadeOnSol API key — Pro/Ultra tier) ──
|
|
344
|
+
const hasRestAuth = authMode === "madeonsol";
|
|
345
|
+
if (hasRestAuth) {
|
|
346
|
+
async function restQuery(method, path, body) {
|
|
347
|
+
const headers = {
|
|
348
|
+
"Content-Type": "application/json",
|
|
349
|
+
...apiKeyHeaders(),
|
|
350
|
+
};
|
|
351
|
+
const res = await fetch(`${BASE_URL}/api/v1${path}`, {
|
|
352
|
+
method,
|
|
353
|
+
headers,
|
|
354
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
355
|
+
});
|
|
356
|
+
if (!res.ok) {
|
|
357
|
+
const text = await res.text().catch(() => "");
|
|
358
|
+
return `Error ${res.status}: ${text}`;
|
|
359
|
+
}
|
|
360
|
+
return JSON.stringify(await res.json(), null, 2);
|
|
361
|
+
}
|
|
362
|
+
const webhookAnnotations = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true };
|
|
363
|
+
server.tool("madeonsol_create_webhook", "Register a webhook URL to receive real-time push notifications for KOL trades and deployer alerts. Requires Pro/Ultra subscription.", {
|
|
364
|
+
url: z.string().url().describe("HTTPS webhook URL to receive events"),
|
|
365
|
+
events: z.array(z.enum(["kol:trade", "kol:coordination", "deployer:alert", "deployer:bond"])).min(1).describe("Event types to subscribe to"),
|
|
366
|
+
min_sol: z.number().optional().describe("Optional: minimum SOL amount filter (for kol:trade)"),
|
|
367
|
+
action: z.enum(["buy", "sell"]).optional().describe("Optional: filter by buy or sell only"),
|
|
368
|
+
deployer_tier: z.array(z.string()).optional().describe("Optional: filter by deployer tiers, e.g. ['elite', 'good']"),
|
|
369
|
+
}, webhookAnnotations, async ({ url, events, min_sol, action, deployer_tier }) => {
|
|
370
|
+
const filters = {};
|
|
371
|
+
if (min_sol)
|
|
372
|
+
filters.min_sol = min_sol;
|
|
373
|
+
if (action)
|
|
374
|
+
filters.action = action;
|
|
375
|
+
if (deployer_tier)
|
|
376
|
+
filters.deployer_tier = deployer_tier;
|
|
377
|
+
return { content: [{ type: "text", text: await restQuery("POST", "/webhooks", { url, events, filters }) }] };
|
|
378
|
+
});
|
|
379
|
+
server.tool("madeonsol_list_webhooks", "List all your registered webhooks with delivery status and failure counts.", {}, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async () => ({
|
|
380
|
+
content: [{ type: "text", text: await restQuery("GET", "/webhooks") }],
|
|
381
|
+
}));
|
|
382
|
+
server.tool("madeonsol_delete_webhook", "Delete a webhook by ID. Permanently removes the webhook and its delivery history.", {
|
|
383
|
+
id: z.number().describe("Webhook ID to delete"),
|
|
384
|
+
}, { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }, async ({ id }) => ({
|
|
385
|
+
content: [{ type: "text", text: await restQuery("DELETE", `/webhooks/${id}`) }],
|
|
386
|
+
}));
|
|
387
|
+
server.tool("madeonsol_test_webhook", "Send a sample event payload to a webhook URL to verify it works. Returns status code and response time.", {
|
|
388
|
+
webhook_id: z.number().describe("ID of the webhook to test"),
|
|
389
|
+
}, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ webhook_id }) => ({
|
|
390
|
+
content: [{ type: "text", text: await restQuery("POST", "/webhooks/test", { webhook_id }) }],
|
|
391
|
+
}));
|
|
392
|
+
server.tool("madeonsol_stream_token", "Generate a 24h WebSocket streaming token. Includes ws_url for KOL/deployer streaming (Pro/Ultra) and dex_ws_url for all-DEX trade streaming (Ultra only).", {}, { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async () => ({
|
|
393
|
+
content: [{ type: "text", text: await restQuery("POST", "/stream/token") }],
|
|
394
|
+
}));
|
|
395
|
+
// ── Alpha wallet intelligence ──
|
|
396
|
+
server.tool("madeonsol_alpha_leaderboard", "Top statistically profitable early-buyer wallets, scored from 47,000+ early-buyer records. BASIC=25 (truncated), PRO=100, ULTRA=500 + bot signals.", {
|
|
397
|
+
period: z.enum(["7d", "30d", "all"]).default("30d").describe("Time window"),
|
|
398
|
+
min_tokens: z.number().min(1).max(20).default(5).describe("Minimum tokens traded by wallet (1-20)"),
|
|
399
|
+
sort: z.enum(["win_rate", "pnl", "roi"]).default("win_rate").describe("Sort axis"),
|
|
400
|
+
exclude_bots: z.boolean().default(true).describe("Exclude wallets flagged as bots"),
|
|
401
|
+
}, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ period, min_tokens, sort, exclude_bots }) => {
|
|
402
|
+
const params = { period, min_tokens, sort, exclude_bots: exclude_bots ? "true" : "false" };
|
|
403
|
+
const url = new URL(`${BASE_URL}/api/v1/alpha/leaderboard`);
|
|
404
|
+
for (const [k, v] of Object.entries(params))
|
|
405
|
+
url.searchParams.set(k, String(v));
|
|
406
|
+
const res = await fetch(url.toString(), { headers: { "Content-Type": "application/json", ...apiKeyHeaders() } });
|
|
407
|
+
const text = res.ok ? JSON.stringify(await res.json(), null, 2) : `Error ${res.status}: ${await res.text().catch(() => "")}`;
|
|
408
|
+
return { content: [{ type: "text", text }] };
|
|
409
|
+
});
|
|
410
|
+
server.tool("madeonsol_alpha_wallet", "Full alpha profile for one wallet — per-token breakdown + bot_signals array. ULTRA only.", { wallet: z.string().describe("Wallet address (base58)") }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ wallet }) => ({
|
|
411
|
+
content: [{ type: "text", text: await restQuery("GET", `/alpha/${encodeURIComponent(wallet)}`) }],
|
|
412
|
+
}));
|
|
413
|
+
server.tool("madeonsol_alpha_linked", "Wallets behaviorally linked to a target wallet (co-bought 3+ tokens within 2 seconds). ULTRA only.", { wallet: z.string().describe("Wallet address (base58)") }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ wallet }) => ({
|
|
414
|
+
content: [{ type: "text", text: await restQuery("GET", `/alpha/${encodeURIComponent(wallet)}/linked`) }],
|
|
415
|
+
}));
|
|
416
|
+
// ── Token quality ──
|
|
417
|
+
server.tool("madeonsol_token_cap_table", "First non-deployer early buyers for a token, enriched with PnL, KOL identity, and bot flags. PRO=top 10 (truncated wallets), ULTRA=top 20 (full). BASIC: 403.", { mint: z.string().describe("Token mint address (base58)") }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ mint }) => ({
|
|
418
|
+
content: [{ type: "text", text: await restQuery("GET", `/tokens/${encodeURIComponent(mint)}/cap-table`) }],
|
|
419
|
+
}));
|
|
420
|
+
server.tool("madeonsol_token_buyer_quality", "0–100 buyer-quality score for a token's first-buyer cohort. 5-min cached. BASIC: score+signal only. PRO/ULTRA: full breakdown.", { mint: z.string().describe("Token mint address (base58)") }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ mint }) => ({
|
|
421
|
+
content: [{ type: "text", text: await restQuery("GET", `/tokens/${encodeURIComponent(mint)}/buyer-quality`) }],
|
|
422
|
+
}));
|
|
423
|
+
server.tool("madeonsol_tokens_batch_buyer_quality", "Bulk buyer-quality scoring for up to 50 mints in one call. Shares the 5-min LRU cache with the single-mint endpoint — already-warm mints return at ~zero cost. Response includes cache_hits counter.", { mints: z.array(z.string()).min(1).max(50).describe("1–50 base58 Solana token mints") }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ mints }) => ({
|
|
424
|
+
content: [{ type: "text", text: await restQuery("POST", "/tokens/batch/buyer-quality", { mints }) }],
|
|
425
|
+
}));
|
|
426
|
+
// ── Token intelligence (/token/{mint} + batch) ──
|
|
427
|
+
server.tool("madeonsol_token_get", "Comprehensive per-mint snapshot: price (VWAP), market cap, 24h volume, deployer reputation, KOL smart-money activity, first_seen_at + age_seconds, and blacklist status — all in one call. ULTRA adds individual KOL wallet addresses in top_buyers[].", { mint: z.string().describe("Token mint address (base58)") }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ mint }) => ({
|
|
428
|
+
content: [{ type: "text", text: await restQuery("GET", `/token/${encodeURIComponent(mint)}`) }],
|
|
429
|
+
}));
|
|
430
|
+
server.tool("madeonsol_token_batch", "Bulk lookup of up to 50 mints in one request. Returns the same per-mint shape as madeonsol_token_get. DB queries batched with IN(...); dex-stream + RPC fan-outs run in parallel. ~10-20× cheaper than N sequential calls — ideal for sniper pipelines scoring many tokens at once.", { mints: z.array(z.string()).min(1).max(50).describe("1–50 base58 Solana token mints") }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ mints }) => ({
|
|
431
|
+
content: [{ type: "text", text: await restQuery("POST", "/token/batch", { mints }) }],
|
|
432
|
+
}));
|
|
433
|
+
// ── Copy-Trade rules (PRO/ULTRA) ──
|
|
434
|
+
server.tool("madeonsol_copytrade_list", "List your copy-trade rules. PRO=3 rules, ULTRA=20 rules.", {}, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async () => ({
|
|
435
|
+
content: [{ type: "text", text: await restQuery("GET", "/copytrade/subscriptions") }],
|
|
436
|
+
}));
|
|
437
|
+
server.tool("madeonsol_copytrade_create", "Create a copy-trade rule. Returns webhook_secret ONCE on creation when delivery_mode includes 'webhook' — store it to verify HMAC signatures. PRO=5 source_wallets/rule, ULTRA=50.", {
|
|
438
|
+
source_wallets: z.array(z.string()).min(1).max(50).describe("Wallets to mirror (base58)"),
|
|
439
|
+
sizing_amount: z.number().describe("Amount used by the chosen sizing_mode"),
|
|
440
|
+
name: z.string().optional().describe("Optional human label"),
|
|
441
|
+
min_trade_sol: z.number().optional().describe("Minimum source-wallet trade size to fire a signal"),
|
|
442
|
+
only_action: z.enum(["buy", "sell", "both"]).optional().describe("Filter to one side (default 'both')"),
|
|
443
|
+
sizing_mode: z.enum(["fixed", "proportional", "percent_source"]).optional().describe("How sizing_amount is interpreted"),
|
|
444
|
+
delivery_mode: z.enum(["webhook", "websocket", "both"]).optional().describe("Where to deliver fired signals"),
|
|
445
|
+
webhook_url: z.string().url().optional().describe("Required when delivery_mode includes 'webhook'"),
|
|
446
|
+
}, { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async (args) => {
|
|
447
|
+
const body = { source_wallets: args.source_wallets, sizing_amount: args.sizing_amount };
|
|
448
|
+
for (const k of ["name", "min_trade_sol", "only_action", "sizing_mode", "delivery_mode", "webhook_url"]) {
|
|
449
|
+
if (args[k] !== undefined)
|
|
450
|
+
body[k] = args[k];
|
|
451
|
+
}
|
|
452
|
+
return { content: [{ type: "text", text: await restQuery("POST", "/copytrade/subscriptions", body) }] };
|
|
453
|
+
});
|
|
454
|
+
server.tool("madeonsol_copytrade_get", "Get one copy-trade rule by id.", { id: z.number().describe("Subscription id") }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ id }) => ({
|
|
455
|
+
content: [{ type: "text", text: await restQuery("GET", `/copytrade/subscriptions/${id}`) }],
|
|
456
|
+
}));
|
|
457
|
+
server.tool("madeonsol_copytrade_update", "Update fields on a copy-trade rule, including is_active toggle.", {
|
|
458
|
+
id: z.number().describe("Subscription id"),
|
|
459
|
+
name: z.string().nullable().optional(),
|
|
460
|
+
source_wallets: z.array(z.string()).optional(),
|
|
461
|
+
min_trade_sol: z.number().optional(),
|
|
462
|
+
only_action: z.enum(["buy", "sell", "both"]).optional(),
|
|
463
|
+
sizing_mode: z.enum(["fixed", "proportional", "percent_source"]).optional(),
|
|
464
|
+
sizing_amount: z.number().optional(),
|
|
465
|
+
delivery_mode: z.enum(["webhook", "websocket", "both"]).optional(),
|
|
466
|
+
webhook_url: z.string().url().nullable().optional(),
|
|
467
|
+
is_active: z.boolean().optional(),
|
|
468
|
+
}, { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async ({ id, ...patch }) => {
|
|
469
|
+
const body = {};
|
|
470
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
471
|
+
if (v !== undefined)
|
|
472
|
+
body[k] = v;
|
|
473
|
+
}
|
|
474
|
+
return { content: [{ type: "text", text: await restQuery("PATCH", `/copytrade/subscriptions/${id}`, body) }] };
|
|
475
|
+
});
|
|
476
|
+
server.tool("madeonsol_copytrade_delete", "Delete a copy-trade rule permanently.", { id: z.number().describe("Subscription id") }, { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }, async ({ id }) => ({
|
|
477
|
+
content: [{ type: "text", text: await restQuery("DELETE", `/copytrade/subscriptions/${id}`) }],
|
|
478
|
+
}));
|
|
479
|
+
server.tool("madeonsol_copytrade_signals", "Recent fired copy-trade signals (up to 7 days). Filter by subscription_id, since (ISO8601), and limit (1–500).", {
|
|
480
|
+
subscription_id: z.number().optional().describe("Filter to one rule"),
|
|
481
|
+
since: z.string().optional().describe("ISO8601 timestamp — only signals fired at-or-after this time"),
|
|
482
|
+
limit: z.number().min(1).max(500).default(50).describe("Max signals to return (1–500)"),
|
|
483
|
+
}, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ subscription_id, since, limit }) => {
|
|
484
|
+
const url = new URL(`${BASE_URL}/api/v1/copytrade/signals`);
|
|
485
|
+
url.searchParams.set("limit", String(limit));
|
|
486
|
+
if (subscription_id !== undefined)
|
|
487
|
+
url.searchParams.set("subscription_id", String(subscription_id));
|
|
488
|
+
if (since)
|
|
489
|
+
url.searchParams.set("since", since);
|
|
490
|
+
const res = await fetch(url.toString(), { headers: { "Content-Type": "application/json", ...apiKeyHeaders() } });
|
|
491
|
+
const text = res.ok ? JSON.stringify(await res.json(), null, 2) : `Error ${res.status}: ${await res.text().catch(() => "")}`;
|
|
492
|
+
return { content: [{ type: "text", text }] };
|
|
493
|
+
});
|
|
494
|
+
// ── Coordination alerts (PRO/ULTRA, v1.1) ──
|
|
495
|
+
server.tool("madeonsol_coordination_alerts_list", "List your coordination alert rules. PRO=5 rules, ULTRA=20.", {}, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async () => ({
|
|
496
|
+
content: [{ type: "text", text: await restQuery("GET", "/kol/coordination/alerts") }],
|
|
497
|
+
}));
|
|
498
|
+
server.tool("madeonsol_coordination_alerts_create", "Create a coordination alert rule. Fires within ~1s when a KOL cluster meets thresholds (peak-density scored). Delivered via WebSocket (kol:coordination channel) and/or HMAC-signed webhook. Returns webhook_secret ONCE when delivery_mode includes 'webhook' — store it.", {
|
|
499
|
+
name: z.string().optional().describe("Optional label"),
|
|
500
|
+
min_kols: z.number().min(2).max(50).optional().describe("Minimum distinct KOLs in the window (default 3)"),
|
|
501
|
+
window_minutes: z.number().min(1).max(60).optional().describe("Peak-density window size in minutes (default 15)"),
|
|
502
|
+
min_score: z.number().min(0).max(100).optional().describe("Minimum composite score 0-100 (default 60)"),
|
|
503
|
+
include_majors: z.boolean().optional().describe("Include WIF/BONK/POPCAT etc. Default false."),
|
|
504
|
+
cooldown_min: z.number().min(1).optional().describe("Silence per (rule, token) in minutes (default 60)"),
|
|
505
|
+
score_jump_break: z.number().min(1).max(100).optional().describe("Re-fire early when score jumps by N points vs last fire (default 10)"),
|
|
506
|
+
delivery_mode: z.enum(["websocket", "webhook", "both"]).optional().describe("Where to deliver fires"),
|
|
507
|
+
webhook_url: z.string().url().optional().describe("Required when delivery_mode includes 'webhook'"),
|
|
508
|
+
}, { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async (args) => {
|
|
509
|
+
const body = {};
|
|
510
|
+
for (const [k, v] of Object.entries(args))
|
|
511
|
+
if (v !== undefined)
|
|
512
|
+
body[k] = v;
|
|
513
|
+
return { content: [{ type: "text", text: await restQuery("POST", "/kol/coordination/alerts", body) }] };
|
|
514
|
+
});
|
|
515
|
+
server.tool("madeonsol_coordination_alerts_get", "Get one coordination alert rule by id.", { id: z.string().describe("Rule UUID") }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ id }) => ({
|
|
516
|
+
content: [{ type: "text", text: await restQuery("GET", `/kol/coordination/alerts/${encodeURIComponent(id)}`) }],
|
|
517
|
+
}));
|
|
518
|
+
server.tool("madeonsol_coordination_alerts_update", "Update fields on a coordination alert rule, including is_active toggle.", {
|
|
519
|
+
id: z.string().describe("Rule UUID"),
|
|
520
|
+
name: z.string().nullable().optional(),
|
|
521
|
+
min_kols: z.number().min(2).max(50).optional(),
|
|
522
|
+
window_minutes: z.number().min(1).max(60).optional(),
|
|
523
|
+
min_score: z.number().min(0).max(100).optional(),
|
|
524
|
+
include_majors: z.boolean().optional(),
|
|
525
|
+
cooldown_min: z.number().min(1).optional(),
|
|
526
|
+
score_jump_break: z.number().min(1).max(100).optional(),
|
|
527
|
+
delivery_mode: z.enum(["websocket", "webhook", "both"]).optional(),
|
|
528
|
+
webhook_url: z.string().url().nullable().optional(),
|
|
529
|
+
is_active: z.boolean().optional(),
|
|
530
|
+
}, { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async ({ id, ...patch }) => {
|
|
531
|
+
const body = {};
|
|
532
|
+
for (const [k, v] of Object.entries(patch))
|
|
533
|
+
if (v !== undefined)
|
|
534
|
+
body[k] = v;
|
|
535
|
+
return { content: [{ type: "text", text: await restQuery("PATCH", `/kol/coordination/alerts/${encodeURIComponent(id)}`, body) }] };
|
|
536
|
+
});
|
|
537
|
+
server.tool("madeonsol_coordination_alerts_delete", "Delete a coordination alert rule permanently.", { id: z.string().describe("Rule UUID") }, { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }, async ({ id }) => ({
|
|
538
|
+
content: [{ type: "text", text: await restQuery("DELETE", `/kol/coordination/alerts/${encodeURIComponent(id)}`) }],
|
|
539
|
+
}));
|
|
540
|
+
// ── First-touch signal (read tool + ULTRA webhook subscriptions CRUD) ──
|
|
541
|
+
server.tool("madeonsol_kol_first_touches", "Recent first-KOL-touch events — every time a tracked KOL was the first to buy a token mint. Filterable by scout tier (S/A/B/C from mv_kol_scout_score), KOL winrate, token age, etc. Backtest: top scouts attract ≥3 follow-on KOLs within 4h ~50% of the time vs ~14% baseline. Median lead time before second KOL is 12s — for trading this signal, use the WebSocket channel rather than polling.", {
|
|
542
|
+
limit: z.number().min(1).max(100).optional().describe("Number of events to return (1-100, default 50)"),
|
|
543
|
+
since: z.string().optional().describe("ISO timestamp — events strictly newer than this. Polling cursor."),
|
|
544
|
+
before: z.string().optional().describe("ISO timestamp — events strictly older than this. Pagination cursor."),
|
|
545
|
+
kol: z.string().optional().describe("Filter to a single KOL wallet address (base58)"),
|
|
546
|
+
min_kol_winrate_7d: z.number().min(0).max(100).optional().describe("Minimum 7d winrate of the first-touch KOL (0-100)"),
|
|
547
|
+
min_scout_tier: z.enum(["S", "A", "B", "C"]).optional().describe("Restrict to first-touch KOLs of this scout tier or better. Requires n_first_touches_30d >= 30."),
|
|
548
|
+
min_n_touches: z.number().min(1).optional().describe("Lower the minimum sample size for scout scoring (default 30)"),
|
|
549
|
+
strategy: z.enum(["scalper", "day_trader", "swing_trader", "hodler", "mixed"]).optional().describe("Filter by first-touch KOL's auto-tagged strategy"),
|
|
550
|
+
token_age_max_min: z.number().min(1).optional().describe("Only events on tokens younger than N minutes (uses token_first_seen)"),
|
|
551
|
+
min_first_buy_sol: z.number().min(0).optional().describe("Minimum size of the first KOL buy in SOL"),
|
|
552
|
+
mint_suffix: z.string().optional().describe("Suffix-filter the token mint (e.g. 'pump', 'bonk')"),
|
|
553
|
+
preset: z.enum(["scout", "fresh_launch"]).optional().describe("Shortcut filter: 'scout' = min_scout_tier=B + min_n_touches=30 + token_age_max_min=60. 'fresh_launch' = token_age_max_min=15."),
|
|
554
|
+
include: z.string().optional().describe("Comma-separated includes — currently 'followers_4h' (computed for events >=4h old)"),
|
|
555
|
+
}, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async (args) => {
|
|
556
|
+
const params = {};
|
|
557
|
+
for (const [k, v] of Object.entries(args))
|
|
558
|
+
if (v !== undefined)
|
|
559
|
+
params[k] = v;
|
|
560
|
+
return { content: [{ type: "text", text: await restQuery("GET", `/kol/first-touches?${new URLSearchParams(params).toString()}`) }] };
|
|
561
|
+
});
|
|
562
|
+
server.tool("madeonsol_first_touch_subscriptions_list", "List your first-touch webhook subscriptions. ULTRA only.", {}, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async () => ({
|
|
563
|
+
content: [{ type: "text", text: await restQuery("GET", "/kol/first-touches/subscriptions") }],
|
|
564
|
+
}));
|
|
565
|
+
server.tool("madeonsol_first_touch_subscriptions_create", "Create a first-touch webhook subscription. ULTRA only — up to 10 active. Filters: kol (wallet), mint_suffix, min_first_buy_sol, min_scout_tier (S/A/B/C), min_n_touches. Returns webhook_secret ONCE — store it.", {
|
|
566
|
+
name: z.string().optional().describe("Optional label"),
|
|
567
|
+
filters: z.object({
|
|
568
|
+
kol: z.string().optional(),
|
|
569
|
+
mint_suffix: z.string().optional(),
|
|
570
|
+
min_first_buy_sol: z.number().min(0).optional(),
|
|
571
|
+
min_scout_tier: z.enum(["S", "A", "B", "C"]).optional(),
|
|
572
|
+
min_n_touches: z.number().min(1).optional(),
|
|
573
|
+
}).optional(),
|
|
574
|
+
delivery_mode: z.enum(["websocket", "webhook", "both"]).optional().describe("Default 'webhook'"),
|
|
575
|
+
webhook_url: z.string().url().optional().describe("Required when delivery_mode includes 'webhook'"),
|
|
576
|
+
}, { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async (args) => {
|
|
577
|
+
const body = {};
|
|
578
|
+
for (const [k, v] of Object.entries(args))
|
|
579
|
+
if (v !== undefined)
|
|
580
|
+
body[k] = v;
|
|
581
|
+
return { content: [{ type: "text", text: await restQuery("POST", "/kol/first-touches/subscriptions", body) }] };
|
|
582
|
+
});
|
|
583
|
+
server.tool("madeonsol_first_touch_subscriptions_get", "Get one first-touch subscription by id. ULTRA only.", { id: z.string().describe("Subscription UUID") }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ id }) => ({
|
|
584
|
+
content: [{ type: "text", text: await restQuery("GET", `/kol/first-touches/subscriptions/${encodeURIComponent(id)}`) }],
|
|
585
|
+
}));
|
|
586
|
+
server.tool("madeonsol_first_touch_subscriptions_update", "Update fields on a first-touch subscription, including is_active toggle. ULTRA only.", {
|
|
587
|
+
id: z.string().describe("Subscription UUID"),
|
|
588
|
+
name: z.string().nullable().optional(),
|
|
589
|
+
filters: z.object({
|
|
590
|
+
kol: z.string().optional(),
|
|
591
|
+
mint_suffix: z.string().optional(),
|
|
592
|
+
min_first_buy_sol: z.number().min(0).optional(),
|
|
593
|
+
min_scout_tier: z.enum(["S", "A", "B", "C"]).optional(),
|
|
594
|
+
min_n_touches: z.number().min(1).optional(),
|
|
595
|
+
}).optional(),
|
|
596
|
+
delivery_mode: z.enum(["websocket", "webhook", "both"]).optional(),
|
|
597
|
+
webhook_url: z.string().url().nullable().optional(),
|
|
598
|
+
is_active: z.boolean().optional(),
|
|
599
|
+
}, { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async ({ id, ...patch }) => {
|
|
600
|
+
const body = {};
|
|
601
|
+
for (const [k, v] of Object.entries(patch))
|
|
602
|
+
if (v !== undefined)
|
|
603
|
+
body[k] = v;
|
|
604
|
+
return { content: [{ type: "text", text: await restQuery("PATCH", `/kol/first-touches/subscriptions/${encodeURIComponent(id)}`, body) }] };
|
|
605
|
+
});
|
|
606
|
+
server.tool("madeonsol_first_touch_subscriptions_delete", "Delete a first-touch subscription permanently. ULTRA only.", { id: z.string().describe("Subscription UUID") }, { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }, async ({ id }) => ({
|
|
607
|
+
content: [{ type: "text", text: await restQuery("DELETE", `/kol/first-touches/subscriptions/${encodeURIComponent(id)}`) }],
|
|
608
|
+
}));
|
|
609
|
+
console.error("[madeonsol-mcp] Webhook & streaming tools enabled");
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
console.error("[madeonsol-mcp] Webhook/streaming tools disabled (requires MADEONSOL_API_KEY)");
|
|
613
|
+
}
|
|
614
|
+
// Prompts — pre-built analysis templates
|
|
615
|
+
server.prompt("solana_kol_analysis", "Analyze current Solana KOL trading activity — what are smart money wallets buying and selling?", { period: z.string().default("24h").describe("Time period: 1h, 6h, 24h, or 7d") }, ({ period }) => ({
|
|
616
|
+
messages: [{
|
|
617
|
+
role: "user",
|
|
618
|
+
content: { type: "text", text: `Analyze Solana KOL activity for the last ${period}. First get the KOL feed for recent trades, then check the coordination signals to see what tokens multiple KOLs are converging on, and finally show the leaderboard to see who's performing best. Summarize the key trends.` },
|
|
619
|
+
}],
|
|
620
|
+
}));
|
|
621
|
+
server.prompt("deployer_scout", "Scout for new high-potential token launches from elite Pump.fun deployers", {}, () => ({
|
|
622
|
+
messages: [{
|
|
623
|
+
role: "user",
|
|
624
|
+
content: { type: "text", text: "Check the latest deployer alerts for new token launches from elite Pump.fun deployers. For each alert, note the deployer tier, bonding rate, and whether any KOLs have already bought in. Highlight the most promising launches." },
|
|
625
|
+
}],
|
|
626
|
+
}));
|
|
627
|
+
// Resources — static info about the API
|
|
628
|
+
server.resource("api-overview", "madeonsol://api-overview", { description: "MadeOnSol x402 API overview — endpoints, pricing, and how it works", mimeType: "application/json" }, async () => {
|
|
629
|
+
const res = await fetch(new URL("/api/x402", BASE_URL).toString());
|
|
630
|
+
const data = await res.json();
|
|
631
|
+
return { contents: [{ uri: "madeonsol://api-overview", text: JSON.stringify(data, null, 2), mimeType: "application/json" }] };
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
async function main() {
|
|
635
|
+
await initAuth();
|
|
636
|
+
if (MODE === "http") {
|
|
637
|
+
// HTTP transport for hosted environments (Smithery, etc.)
|
|
638
|
+
const httpServer = createServer();
|
|
639
|
+
const transports = new Map();
|
|
640
|
+
httpServer.on("request", async (req, res) => {
|
|
641
|
+
// Health check
|
|
642
|
+
if (req.method === "GET" && req.url === "/health") {
|
|
643
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
644
|
+
res.end(JSON.stringify({ status: "ok", server: "madeonsol-mcp" }));
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
// Smithery server card for discovery
|
|
648
|
+
if (req.method === "GET" && req.url === "/.well-known/mcp/server-card.json") {
|
|
649
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
650
|
+
res.end(JSON.stringify({
|
|
651
|
+
name: "madeonsol",
|
|
652
|
+
description: "Solana KOL trading intelligence and deployer analytics. Real-time data from 1,000+ KOL wallets, 6,700+ Pump.fun deployers, 47,000+ scored alpha wallets, copy-trade rules, and wallet tracker. Supports MadeOnSol API key (msk_) or x402 micropayments.",
|
|
653
|
+
version: "1.3.0",
|
|
654
|
+
tools: [
|
|
655
|
+
{ name: "madeonsol_kol_feed", description: "Get real-time Solana KOL trades from 1,000+ tracked wallets." },
|
|
656
|
+
{ name: "madeonsol_kol_coordination", description: "Get KOL convergence signals — tokens multiple KOLs are accumulating." },
|
|
657
|
+
{ name: "madeonsol_kol_leaderboard", description: "Get KOL performance rankings by PnL and win rate." },
|
|
658
|
+
{ name: "madeonsol_deployer_alerts", description: "Get elite Pump.fun deployer alerts with KOL enrichment." },
|
|
659
|
+
{ name: "madeonsol_kol_pairs", description: "KOL affinity matrix — which KOLs co-trade the same tokens." },
|
|
660
|
+
{ name: "madeonsol_kol_timing", description: "KOL entry/exit timing profile. Pro/Ultra." },
|
|
661
|
+
{ name: "madeonsol_deployer_trajectory", description: "Deployer skill curve — streaks, trend. Pro/Ultra." },
|
|
662
|
+
{ name: "madeonsol_kol_hot_tokens", description: "KOL momentum tokens — accelerating buy interest." },
|
|
663
|
+
{ name: "madeonsol_kol_pnl", description: "Deep per-wallet PnL: equity curve, risk metrics, positions." },
|
|
664
|
+
{ name: "madeonsol_kol_trending_tokens", description: "Tokens ranked by KOL buy volume (5m–12h windows)." },
|
|
665
|
+
{ name: "madeonsol_kol_token_entry_order", description: "Ranked KOL first-buyers for a specific token." },
|
|
666
|
+
{ name: "madeonsol_kol_compare_wallets", description: "Side-by-side comparison of 2-5 KOL wallets (overlap in PRO+)." },
|
|
667
|
+
{ name: "madeonsol_kol_alerts_recent", description: "Unified live KOL alert feed: clusters, fresh buys, heating-up." },
|
|
668
|
+
{ name: "madeonsol_discovery", description: "List all available endpoints with prices. Free." },
|
|
669
|
+
{ name: "madeonsol_create_webhook", description: "Register a webhook for real-time push notifications. Pro/Ultra." },
|
|
670
|
+
{ name: "madeonsol_list_webhooks", description: "List your registered webhooks. Pro/Ultra." },
|
|
671
|
+
{ name: "madeonsol_delete_webhook", description: "Delete a webhook by ID. Pro/Ultra." },
|
|
672
|
+
{ name: "madeonsol_test_webhook", description: "Send a test payload to verify a webhook. Pro/Ultra." },
|
|
673
|
+
{ name: "madeonsol_stream_token", description: "Get a 24h WebSocket streaming token. Pro/Ultra." },
|
|
674
|
+
{ name: "madeonsol_wallet_tracker_watchlist", description: "List your tracked wallets and remaining capacity." },
|
|
675
|
+
{ name: "madeonsol_wallet_tracker_add", description: "Add a wallet to your watchlist." },
|
|
676
|
+
{ name: "madeonsol_wallet_tracker_remove", description: "Remove a wallet from your watchlist." },
|
|
677
|
+
{ name: "madeonsol_wallet_tracker_trades", description: "Historical swap/transfer events for watched wallets." },
|
|
678
|
+
{ name: "madeonsol_wallet_tracker_summary", description: "Per-wallet stats: swap counts, SOL bought/sold." },
|
|
679
|
+
{ name: "madeonsol_alpha_leaderboard", description: "Top profitable early-buyer wallets — 47,000+ scored. BASIC=25, PRO=100, ULTRA=500." },
|
|
680
|
+
{ name: "madeonsol_alpha_wallet", description: "Full alpha profile + bot signals for one wallet. ULTRA only." },
|
|
681
|
+
{ name: "madeonsol_alpha_linked", description: "Behaviorally linked wallets (co-bought 3+ tokens within 2s). ULTRA only." },
|
|
682
|
+
{ name: "madeonsol_token_cap_table", description: "First non-deployer early buyers for a token, enriched. PRO=10, ULTRA=20." },
|
|
683
|
+
{ name: "madeonsol_token_buyer_quality", description: "0–100 buyer quality score for a token's first-buyer cohort." },
|
|
684
|
+
{ name: "madeonsol_tokens_batch_buyer_quality", description: "Bulk buyer-quality scoring for up to 50 mints. Shares the LRU cache." },
|
|
685
|
+
{ name: "madeonsol_token_get", description: "Comprehensive per-mint snapshot: price, MC, volume, deployer, KOL, age, blacklist." },
|
|
686
|
+
{ name: "madeonsol_token_batch", description: "Bulk token snapshot for up to 50 mints — ~10-20× cheaper than N sequential calls." },
|
|
687
|
+
{ name: "madeonsol_copytrade_list", description: "List your copy-trade rules. PRO/ULTRA." },
|
|
688
|
+
{ name: "madeonsol_copytrade_create", description: "Create a copy-trade rule with webhook + WS delivery. PRO/ULTRA." },
|
|
689
|
+
{ name: "madeonsol_copytrade_get", description: "Get one copy-trade rule. PRO/ULTRA." },
|
|
690
|
+
{ name: "madeonsol_copytrade_update", description: "Update a copy-trade rule. PRO/ULTRA." },
|
|
691
|
+
{ name: "madeonsol_copytrade_delete", description: "Delete a copy-trade rule. PRO/ULTRA." },
|
|
692
|
+
{ name: "madeonsol_copytrade_signals", description: "Recent fired copy-trade signals (up to 7 days). PRO/ULTRA." },
|
|
693
|
+
{ name: "madeonsol_kol_first_touches", description: "Recent first-KOL-touch events on tokens — backtested scout signal. Filterable by scout tier S/A/B/C, KOL winrate, token age, mint suffix." },
|
|
694
|
+
{ name: "madeonsol_first_touch_subscriptions_list", description: "List your first-touch webhook subscriptions. ULTRA only." },
|
|
695
|
+
{ name: "madeonsol_first_touch_subscriptions_create", description: "Create a first-touch webhook subscription with HMAC signing. ULTRA only." },
|
|
696
|
+
{ name: "madeonsol_first_touch_subscriptions_get", description: "Get one first-touch subscription. ULTRA only." },
|
|
697
|
+
{ name: "madeonsol_first_touch_subscriptions_update", description: "Update a first-touch subscription. ULTRA only." },
|
|
698
|
+
{ name: "madeonsol_first_touch_subscriptions_delete", description: "Delete a first-touch subscription. ULTRA only." },
|
|
699
|
+
{ name: "madeonsol_coordination_alerts_list", description: "List your KOL coordination alert rules. PRO/ULTRA." },
|
|
700
|
+
{ name: "madeonsol_coordination_alerts_create", description: "Create a coordination alert rule (push via WS + webhook, <1s latency). PRO/ULTRA." },
|
|
701
|
+
{ name: "madeonsol_coordination_alerts_get", description: "Get one coordination alert rule. PRO/ULTRA." },
|
|
702
|
+
{ name: "madeonsol_coordination_alerts_update", description: "Update fields on a coordination alert rule. PRO/ULTRA." },
|
|
703
|
+
{ name: "madeonsol_coordination_alerts_delete", description: "Delete a coordination alert rule. PRO/ULTRA." },
|
|
704
|
+
],
|
|
705
|
+
homepage: "https://madeonsol.com/solana-api",
|
|
706
|
+
repository: "https://github.com/LamboPoewert/mcp-server-madeonsol",
|
|
707
|
+
}));
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
// MCP endpoint
|
|
711
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
712
|
+
{
|
|
713
|
+
if (req.method === "POST") {
|
|
714
|
+
let transport = sessionId ? transports.get(sessionId) : undefined;
|
|
715
|
+
if (!transport) {
|
|
716
|
+
transport = new StreamableHTTPServerTransport({
|
|
717
|
+
sessionIdGenerator: undefined,
|
|
718
|
+
});
|
|
719
|
+
const server = new McpServer({ name: "madeonsol", version: "1.1.2" });
|
|
720
|
+
registerTools(server);
|
|
721
|
+
await server.connect(transport);
|
|
722
|
+
}
|
|
723
|
+
await transport.handleRequest(req, res);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
if (req.method === "GET" && sessionId) {
|
|
727
|
+
const transport = transports.get(sessionId);
|
|
728
|
+
if (transport) {
|
|
729
|
+
await transport.handleRequest(req, res);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
if (req.method === "DELETE" && sessionId) {
|
|
734
|
+
const transport = transports.get(sessionId);
|
|
735
|
+
if (transport) {
|
|
736
|
+
await transport.handleRequest(req, res);
|
|
737
|
+
transports.delete(sessionId);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
res.writeHead(404);
|
|
743
|
+
res.end("Not found");
|
|
744
|
+
});
|
|
745
|
+
// Bind to 127.0.0.1 only — defense in depth. UFW already blocks the port
|
|
746
|
+
// externally, but binding to all interfaces would expose the server to any
|
|
747
|
+
// misconfigured firewall rule. Override with HOST=0.0.0.0 if you ever need
|
|
748
|
+
// to expose it directly (e.g. for hosted environments behind a separate
|
|
749
|
+
// reverse proxy).
|
|
750
|
+
const HOST = process.env.HOST || "127.0.0.1";
|
|
751
|
+
httpServer.listen(PORT, HOST, () => {
|
|
752
|
+
console.error(`[madeonsol-mcp] HTTP server listening on ${HOST}:${PORT}`);
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
// Stdio transport for local use (Claude Desktop, Cursor, Claude Code)
|
|
757
|
+
const server = new McpServer({ name: "madeonsol", version: "1.1.2" });
|
|
758
|
+
registerTools(server);
|
|
759
|
+
const transport = new StdioServerTransport();
|
|
760
|
+
await server.connect(transport);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
main().catch(console.error);
|
package/package.json
CHANGED