@fundtracer/mcp 1.0.11 → 1.0.13
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/fundtracer-mcp.js +1950 -438
- package/package.json +5 -4
package/fundtracer-mcp.js
CHANGED
|
@@ -1,226 +1,881 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
};
|
|
2
|
+
|
|
3
|
+
// src/mcp/stdio.ts
|
|
4
|
+
import * as dotenv from "dotenv";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
|
|
7
|
+
// src/mcp/server.ts
|
|
8
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
+
import { z } from "zod";
|
|
11
10
|
|
|
12
11
|
// src/mcp/tools.ts
|
|
13
|
-
var
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
properties: {
|
|
31
|
-
address: { type: "string", description: "Wallet address to analyze (0x... for EVM, base58 for Solana)" },
|
|
32
|
-
chainId: {
|
|
33
|
-
type: "string",
|
|
34
|
-
description: "Blockchain to analyze",
|
|
35
|
-
enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", "solana"]
|
|
36
|
-
},
|
|
37
|
-
transactionLimit: {
|
|
38
|
-
type: "number",
|
|
39
|
-
description: "Max transactions to fetch (default: 500)",
|
|
40
|
-
default: 500
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
|
-
required: ["address", "chainId"]
|
|
12
|
+
var ALL_MCP_TOOLS = [
|
|
13
|
+
{
|
|
14
|
+
name: "analyze_wallet",
|
|
15
|
+
description: "Perform a full blockchain wallet analysis including balance, transactions, risk score, suspicious indicators, and project interactions.",
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
address: { type: "string", description: "Wallet address to analyze (0x... for EVM, base58 for Solana)" },
|
|
20
|
+
chainId: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "Blockchain to analyze",
|
|
23
|
+
enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", "solana"]
|
|
24
|
+
},
|
|
25
|
+
transactionLimit: {
|
|
26
|
+
type: "number",
|
|
27
|
+
description: "Max transactions to fetch (default: 500)",
|
|
28
|
+
default: 500
|
|
44
29
|
}
|
|
45
30
|
},
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
31
|
+
required: ["address", "chainId"]
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "trace_funds",
|
|
36
|
+
description: "Trace funding sources and destinations for a wallet address, building a recursive funding tree.",
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
address: { type: "string", description: "Wallet address to trace" },
|
|
41
|
+
chainId: {
|
|
42
|
+
type: "string",
|
|
43
|
+
description: "Blockchain to trace on",
|
|
44
|
+
enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", "solana"]
|
|
45
|
+
},
|
|
46
|
+
maxDepth: {
|
|
47
|
+
type: "number",
|
|
48
|
+
description: "How many levels deep to trace (default: 3)",
|
|
49
|
+
default: 3
|
|
50
|
+
},
|
|
51
|
+
direction: {
|
|
52
|
+
type: "string",
|
|
53
|
+
description: "Which direction to trace",
|
|
54
|
+
enum: ["sources", "destinations", "both"],
|
|
55
|
+
default: "both"
|
|
71
56
|
}
|
|
72
57
|
},
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
58
|
+
required: ["address", "chainId"]
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "compare_wallets",
|
|
63
|
+
description: "Compare multiple wallet addresses for common funding sources, shared project interactions, and sybil correlation scoring.",
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
addresses: {
|
|
68
|
+
type: "string",
|
|
69
|
+
description: "Comma-separated list of wallet addresses to compare (2-20 wallets)"
|
|
70
|
+
},
|
|
71
|
+
chainId: {
|
|
72
|
+
type: "string",
|
|
73
|
+
description: "Blockchain to compare on",
|
|
74
|
+
enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", "solana"]
|
|
90
75
|
}
|
|
91
76
|
},
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
77
|
+
required: ["addresses", "chainId"]
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "analyze_contract",
|
|
82
|
+
description: "Analyze all addresses that have interacted with a smart contract, detecting sybil clusters and shared funding sources.",
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
contractAddress: { type: "string", description: "Smart contract address to analyze" },
|
|
87
|
+
chainId: {
|
|
88
|
+
type: "string",
|
|
89
|
+
description: "Blockchain the contract is on",
|
|
90
|
+
enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc"]
|
|
91
|
+
},
|
|
92
|
+
maxInteractors: {
|
|
93
|
+
type: "number",
|
|
94
|
+
description: "Max interactors to analyze (default: 100)",
|
|
95
|
+
default: 100
|
|
111
96
|
}
|
|
112
97
|
},
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
98
|
+
required: ["contractAddress", "chainId"]
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "detect_sybil_clusters",
|
|
103
|
+
description: "Detect sybil (fake) accounts by clustering wallets that share common funding sources.",
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: "object",
|
|
106
|
+
properties: {
|
|
107
|
+
addresses: {
|
|
108
|
+
type: "string",
|
|
109
|
+
description: "Comma-separated list of wallet addresses to check for sybil clustering"
|
|
110
|
+
},
|
|
111
|
+
chainId: {
|
|
112
|
+
type: "string",
|
|
113
|
+
description: "Blockchain to analyze on",
|
|
114
|
+
enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "bsc"]
|
|
130
115
|
}
|
|
131
116
|
},
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
117
|
+
required: ["addresses", "chainId"]
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "get_portfolio",
|
|
122
|
+
description: "Get the token portfolio, DeFi positions, and NFT holdings for a wallet address.",
|
|
123
|
+
inputSchema: {
|
|
124
|
+
type: "object",
|
|
125
|
+
properties: {
|
|
126
|
+
address: { type: "string", description: "Wallet address" },
|
|
127
|
+
chainId: {
|
|
128
|
+
type: "string",
|
|
129
|
+
description: "Blockchain",
|
|
130
|
+
enum: ["ethereum", "solana", "base", "arbitrum", "optimism", "polygon", "linea"]
|
|
146
131
|
}
|
|
147
132
|
},
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
description: "Number of transactions to return (default: 50)",
|
|
163
|
-
default: 50
|
|
164
|
-
}
|
|
165
|
-
},
|
|
166
|
-
required: ["address", "chainId"]
|
|
133
|
+
required: ["address", "chainId"]
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "get_portfolio_tokens",
|
|
138
|
+
description: "Get only the token holdings for a wallet address.",
|
|
139
|
+
inputSchema: {
|
|
140
|
+
type: "object",
|
|
141
|
+
properties: {
|
|
142
|
+
address: { type: "string", description: "Wallet address" },
|
|
143
|
+
chainId: {
|
|
144
|
+
type: "string",
|
|
145
|
+
description: "Blockchain",
|
|
146
|
+
enum: ["ethereum", "solana", "base", "arbitrum", "optimism", "polygon", "linea"]
|
|
167
147
|
}
|
|
168
148
|
},
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
required: ["query"]
|
|
149
|
+
required: ["address", "chainId"]
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: "get_portfolio_nfts",
|
|
154
|
+
description: "Get only the NFT holdings for a wallet address.",
|
|
155
|
+
inputSchema: {
|
|
156
|
+
type: "object",
|
|
157
|
+
properties: {
|
|
158
|
+
address: { type: "string", description: "Wallet address" },
|
|
159
|
+
chainId: {
|
|
160
|
+
type: "string",
|
|
161
|
+
description: "Blockchain",
|
|
162
|
+
enum: ["ethereum", "solana", "base", "arbitrum", "optimism", "polygon", "linea"]
|
|
184
163
|
}
|
|
185
164
|
},
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
165
|
+
required: ["address", "chainId"]
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: "get_portfolio_activity",
|
|
170
|
+
description: "Get portfolio activity summary for a wallet address.",
|
|
171
|
+
inputSchema: {
|
|
172
|
+
type: "object",
|
|
173
|
+
properties: {
|
|
174
|
+
address: { type: "string", description: "Wallet address" },
|
|
175
|
+
chainId: {
|
|
176
|
+
type: "string",
|
|
177
|
+
description: "Blockchain",
|
|
178
|
+
enum: ["ethereum", "solana", "base", "arbitrum", "optimism", "polygon", "linea"]
|
|
200
179
|
}
|
|
201
180
|
},
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
181
|
+
required: ["address", "chainId"]
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: "get_portfolio_stablecoins",
|
|
186
|
+
description: "Get stablecoin balances for a wallet address.",
|
|
187
|
+
inputSchema: {
|
|
188
|
+
type: "object",
|
|
189
|
+
properties: {
|
|
190
|
+
address: { type: "string", description: "Wallet address" },
|
|
191
|
+
chainId: {
|
|
192
|
+
type: "string",
|
|
193
|
+
description: "Blockchain",
|
|
194
|
+
enum: ["ethereum", "solana", "base", "arbitrum", "optimism", "polygon", "linea"]
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
required: ["address", "chainId"]
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: "get_transactions",
|
|
202
|
+
description: "Get recent transaction history for a wallet address.",
|
|
203
|
+
inputSchema: {
|
|
204
|
+
type: "object",
|
|
205
|
+
properties: {
|
|
206
|
+
address: { type: "string", description: "Wallet address" },
|
|
207
|
+
chainId: {
|
|
208
|
+
type: "string",
|
|
209
|
+
description: "Blockchain",
|
|
210
|
+
enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", "solana"]
|
|
211
|
+
},
|
|
212
|
+
limit: {
|
|
213
|
+
type: "number",
|
|
214
|
+
description: "Number of transactions to return (default: 50)",
|
|
215
|
+
default: 50
|
|
216
216
|
}
|
|
217
|
+
},
|
|
218
|
+
required: ["address", "chainId"]
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: "lookup_entity",
|
|
223
|
+
description: "Look up a known blockchain entity, protocol, or address label.",
|
|
224
|
+
inputSchema: {
|
|
225
|
+
type: "object",
|
|
226
|
+
properties: {
|
|
227
|
+
query: { type: "string", description: "Entity name, address, or label to look up" },
|
|
228
|
+
chainId: {
|
|
229
|
+
type: "string",
|
|
230
|
+
description: "Blockchain to search (optional)",
|
|
231
|
+
enum: ["ethereum", "solana", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", ""],
|
|
232
|
+
default: ""
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
required: ["query"]
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: "get_gas_prices",
|
|
240
|
+
description: "Get current gas prices across supported blockchain networks.",
|
|
241
|
+
inputSchema: {
|
|
242
|
+
type: "object",
|
|
243
|
+
properties: {
|
|
244
|
+
chainId: {
|
|
245
|
+
type: "string",
|
|
246
|
+
description: "Specific chain (optional, returns all if omitted)",
|
|
247
|
+
enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", ""],
|
|
248
|
+
default: ""
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
required: []
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: "get_token_info",
|
|
256
|
+
description: "Get market data and information for a token by address or symbol.",
|
|
257
|
+
inputSchema: {
|
|
258
|
+
type: "object",
|
|
259
|
+
properties: {
|
|
260
|
+
tokenAddress: { type: "string", description: "Token contract address" },
|
|
261
|
+
chainId: {
|
|
262
|
+
type: "string",
|
|
263
|
+
description: "Blockchain the token is on",
|
|
264
|
+
enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", "solana"]
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
required: ["tokenAddress", "chainId"]
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: "resolve_name",
|
|
272
|
+
description: "Resolve an ENS, Basename, or Linea name to an address.",
|
|
273
|
+
inputSchema: {
|
|
274
|
+
type: "object",
|
|
275
|
+
properties: {
|
|
276
|
+
input: { type: "string", description: "ENS or address-like input to resolve" }
|
|
277
|
+
},
|
|
278
|
+
required: ["input"]
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: "get_transaction_detail",
|
|
283
|
+
description: "Fetch full transaction details, logs, gas costs, and decoded events.",
|
|
284
|
+
inputSchema: {
|
|
285
|
+
type: "object",
|
|
286
|
+
properties: {
|
|
287
|
+
chainId: { type: "string", description: "Blockchain to inspect", enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc"] },
|
|
288
|
+
hash: { type: "string", description: "Transaction hash" }
|
|
289
|
+
},
|
|
290
|
+
required: ["chainId", "hash"]
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: "get_transaction_history",
|
|
295
|
+
description: "Fetch transaction history for a wallet with optional pagination and filters.",
|
|
296
|
+
inputSchema: {
|
|
297
|
+
type: "object",
|
|
298
|
+
properties: {
|
|
299
|
+
address: { type: "string", description: "Wallet address to inspect" },
|
|
300
|
+
chainId: { type: "string", description: "Blockchain", enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", "solana"] },
|
|
301
|
+
pageToken: { type: "string", description: "Pagination token returned by the history endpoint" },
|
|
302
|
+
filters: { type: "object", description: "Optional history filters", default: {} }
|
|
303
|
+
},
|
|
304
|
+
required: ["address", "chainId"]
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
name: "analyze_cex_flow",
|
|
309
|
+
description: "Analyze exchange inflows/outflows and connected wallets for a target wallet.",
|
|
310
|
+
inputSchema: {
|
|
311
|
+
type: "object",
|
|
312
|
+
properties: {
|
|
313
|
+
walletAddress: { type: "string", description: "Wallet address to analyze" },
|
|
314
|
+
chainId: { type: "string", description: "Blockchain", enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc"] },
|
|
315
|
+
cexName: { type: "string", description: "Optional exchange name filter" },
|
|
316
|
+
depth: { type: "number", description: "Traversal depth", default: 3 }
|
|
317
|
+
},
|
|
318
|
+
required: ["walletAddress", "chainId"]
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
name: "analyze_sybil_addresses",
|
|
323
|
+
description: "Analyze a list of wallet addresses for Sybil or coordination patterns.",
|
|
324
|
+
inputSchema: {
|
|
325
|
+
type: "object",
|
|
326
|
+
properties: {
|
|
327
|
+
addresses: { type: "string", description: "Comma-separated wallet addresses" },
|
|
328
|
+
chainId: { type: "string", description: "Blockchain", enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc"] },
|
|
329
|
+
txHash: { type: "string", description: "Optional transaction hash for context" }
|
|
330
|
+
},
|
|
331
|
+
required: ["addresses", "chainId"]
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: "search_contracts",
|
|
336
|
+
description: "Search the contract database by name or symbol.",
|
|
337
|
+
inputSchema: {
|
|
338
|
+
type: "object",
|
|
339
|
+
properties: {
|
|
340
|
+
query: { type: "string", description: "Search term" }
|
|
341
|
+
},
|
|
342
|
+
required: ["query"]
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: "lookup_contract",
|
|
347
|
+
description: "Look up contract metadata and labels by address.",
|
|
348
|
+
inputSchema: {
|
|
349
|
+
type: "object",
|
|
350
|
+
properties: {
|
|
351
|
+
address: { type: "string", description: "Contract address to look up" }
|
|
352
|
+
},
|
|
353
|
+
required: ["address"]
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
name: "batch_lookup_contracts",
|
|
358
|
+
description: "Look up multiple contract addresses at once.",
|
|
359
|
+
inputSchema: {
|
|
360
|
+
type: "object",
|
|
361
|
+
properties: {
|
|
362
|
+
addresses: { type: "string", description: "Comma-separated list of contract addresses" }
|
|
363
|
+
},
|
|
364
|
+
required: ["addresses"]
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
name: "get_contract_stats",
|
|
369
|
+
description: "Get contract database statistics.",
|
|
370
|
+
inputSchema: {
|
|
371
|
+
type: "object",
|
|
372
|
+
properties: {}
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
name: "refresh_contracts",
|
|
377
|
+
description: "Trigger a contract database refresh.",
|
|
378
|
+
inputSchema: {
|
|
379
|
+
type: "object",
|
|
380
|
+
properties: {}
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
name: "search_tokens",
|
|
385
|
+
description: "Search token metadata by keyword.",
|
|
386
|
+
inputSchema: {
|
|
387
|
+
type: "object",
|
|
388
|
+
properties: {
|
|
389
|
+
query: { type: "string", description: "Token search query" }
|
|
390
|
+
},
|
|
391
|
+
required: ["query"]
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
name: "get_market_stats",
|
|
396
|
+
description: "Fetch market overview statistics.",
|
|
397
|
+
inputSchema: {
|
|
398
|
+
type: "object",
|
|
399
|
+
properties: {}
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
name: "get_market_coins",
|
|
404
|
+
description: "Get top market coins with optional chain filtering.",
|
|
405
|
+
inputSchema: {
|
|
406
|
+
type: "object",
|
|
407
|
+
properties: {
|
|
408
|
+
chainId: { type: "string", description: "Optional chain filter", enum: ["ethereum", "linea", "arbitrum", "optimism", "base", "polygon", "bsc", "all"], default: "all" },
|
|
409
|
+
page: { type: "number", description: "Page number", default: 1 },
|
|
410
|
+
perPage: { type: "number", description: "Results per page", default: 100 }
|
|
217
411
|
}
|
|
218
|
-
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
name: "get_token_chart",
|
|
416
|
+
description: "Fetch token chart/price history from the token endpoint.",
|
|
417
|
+
inputSchema: {
|
|
418
|
+
type: "object",
|
|
419
|
+
properties: {
|
|
420
|
+
tokenAddress: { type: "string", description: "Token address" },
|
|
421
|
+
chainId: { type: "string", description: "Blockchain", enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc"] },
|
|
422
|
+
coinId: { type: "string", description: "CoinGecko coin ID" },
|
|
423
|
+
days: { type: "number", description: "Number of days of history", default: 7 }
|
|
424
|
+
},
|
|
425
|
+
required: ["tokenAddress", "chainId", "coinId"]
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
name: "get_dexscreener_latest_profiles",
|
|
430
|
+
description: "Get the latest DEX Screener token profiles.",
|
|
431
|
+
inputSchema: { type: "object", properties: {} }
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
name: "get_dexscreener_top_boosts",
|
|
435
|
+
description: "Get top boosted tokens from DEX Screener.",
|
|
436
|
+
inputSchema: { type: "object", properties: {} }
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
name: "search_dexscreener_pairs",
|
|
440
|
+
description: "Search DEX Screener pairs by query.",
|
|
441
|
+
inputSchema: {
|
|
442
|
+
type: "object",
|
|
443
|
+
properties: { query: { type: "string", description: "Search term" } },
|
|
444
|
+
required: ["query"]
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
name: "get_dexscreener_token_details",
|
|
449
|
+
description: "Get detailed DEX Screener information for a token.",
|
|
450
|
+
inputSchema: {
|
|
451
|
+
type: "object",
|
|
452
|
+
properties: {
|
|
453
|
+
chainId: { type: "string", description: "Chain ID", enum: ["ethereum", "linea", "arbitrum", "optimism", "base", "polygon", "bsc"] },
|
|
454
|
+
tokenAddress: { type: "string", description: "Token address" }
|
|
455
|
+
},
|
|
456
|
+
required: ["chainId", "tokenAddress"]
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
name: "get_dexscreener_token_pairs",
|
|
461
|
+
description: "Get DEX Screener pairs for a token.",
|
|
462
|
+
inputSchema: {
|
|
463
|
+
type: "object",
|
|
464
|
+
properties: {
|
|
465
|
+
chainId: { type: "string", description: "Chain ID", enum: ["ethereum", "linea", "arbitrum", "optimism", "base", "polygon", "bsc"] },
|
|
466
|
+
tokenAddress: { type: "string", description: "Token address" }
|
|
467
|
+
},
|
|
468
|
+
required: ["chainId", "tokenAddress"]
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
name: "get_dexscreener_trending",
|
|
473
|
+
description: "Get trending DEX Screener tokens.",
|
|
474
|
+
inputSchema: { type: "object", properties: {} }
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
name: "get_scan_history",
|
|
478
|
+
description: "Get the authenticated user scan history.",
|
|
479
|
+
inputSchema: { type: "object", properties: {} }
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
name: "get_scan_history_stats",
|
|
483
|
+
description: "Get aggregate scan history stats.",
|
|
484
|
+
inputSchema: { type: "object", properties: {} }
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
name: "save_scan_history_item",
|
|
488
|
+
description: "Save a scan history item for the authenticated user.",
|
|
489
|
+
inputSchema: {
|
|
490
|
+
type: "object",
|
|
491
|
+
properties: {
|
|
492
|
+
address: { type: "string", description: "Address being saved" },
|
|
493
|
+
chain: { type: "string", description: "Chain name", default: "ethereum" },
|
|
494
|
+
label: { type: "string", description: "Optional label" },
|
|
495
|
+
type: { type: "string", description: "Item type", enum: ["wallet", "contract", "compare", "sybil"], default: "wallet" },
|
|
496
|
+
timestamp: { type: "number", description: "Unix timestamp", default: 0 },
|
|
497
|
+
riskScore: { type: "number", description: "Optional risk score" },
|
|
498
|
+
riskLevel: { type: "string", description: "Optional risk level" },
|
|
499
|
+
totalTransactions: { type: "number", description: "Optional transaction count" },
|
|
500
|
+
totalValueSentEth: { type: "number", description: "Optional sent value" },
|
|
501
|
+
totalValueReceivedEth: { type: "number", description: "Optional received value" },
|
|
502
|
+
activityPeriodDays: { type: "number", description: "Optional activity period" },
|
|
503
|
+
balanceInEth: { type: "number", description: "Optional balance" }
|
|
504
|
+
},
|
|
505
|
+
required: ["address"]
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
name: "sync_scan_history",
|
|
510
|
+
description: "Bulk sync scan history items.",
|
|
511
|
+
inputSchema: {
|
|
512
|
+
type: "object",
|
|
513
|
+
properties: {
|
|
514
|
+
items: { type: "array", description: "History items to sync" }
|
|
515
|
+
},
|
|
516
|
+
required: ["items"]
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
name: "delete_scan_history_item",
|
|
521
|
+
description: "Delete a scan history item by address.",
|
|
522
|
+
inputSchema: {
|
|
523
|
+
type: "object",
|
|
524
|
+
properties: {
|
|
525
|
+
address: { type: "string", description: "Address to delete" }
|
|
526
|
+
},
|
|
527
|
+
required: ["address"]
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
name: "clear_scan_history",
|
|
532
|
+
description: "Clear all scan history for the authenticated user.",
|
|
533
|
+
inputSchema: { type: "object", properties: {} }
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
name: "list_rooms",
|
|
537
|
+
description: "List investigation rooms the user belongs to.",
|
|
538
|
+
inputSchema: { type: "object", properties: {} }
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
name: "get_room_details",
|
|
542
|
+
description: "Get room details and members.",
|
|
543
|
+
inputSchema: {
|
|
544
|
+
type: "object",
|
|
545
|
+
properties: { roomId: { type: "string", description: "Room ID" } },
|
|
546
|
+
required: ["roomId"]
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
name: "create_room",
|
|
551
|
+
description: "Create a new investigation room.",
|
|
552
|
+
inputSchema: {
|
|
553
|
+
type: "object",
|
|
554
|
+
properties: {
|
|
555
|
+
name: { type: "string", description: "Room name" },
|
|
556
|
+
description: { type: "string", description: "Room description" },
|
|
557
|
+
seedAddress: { type: "string", description: "Seed wallet or contract address" },
|
|
558
|
+
seedChain: { type: "string", description: "Seed chain" },
|
|
559
|
+
seedSnapshot: { type: "object", description: "Optional seed snapshot" }
|
|
560
|
+
},
|
|
561
|
+
required: ["name"]
|
|
562
|
+
}
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
name: "update_room",
|
|
566
|
+
description: "Update room metadata.",
|
|
567
|
+
inputSchema: {
|
|
568
|
+
type: "object",
|
|
569
|
+
properties: {
|
|
570
|
+
roomId: { type: "string", description: "Room ID" },
|
|
571
|
+
name: { type: "string", description: "New room name" },
|
|
572
|
+
description: { type: "string", description: "New room description" }
|
|
573
|
+
},
|
|
574
|
+
required: ["roomId"]
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
name: "delete_room",
|
|
579
|
+
description: "Delete a room.",
|
|
580
|
+
inputSchema: {
|
|
581
|
+
type: "object",
|
|
582
|
+
properties: { roomId: { type: "string", description: "Room ID" } },
|
|
583
|
+
required: ["roomId"]
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
name: "list_room_messages",
|
|
588
|
+
description: "List room messages.",
|
|
589
|
+
inputSchema: {
|
|
590
|
+
type: "object",
|
|
591
|
+
properties: {
|
|
592
|
+
roomId: { type: "string", description: "Room ID" },
|
|
593
|
+
limit: { type: "number", description: "Message limit", default: 50 },
|
|
594
|
+
before: { type: "number", description: "Load messages before this timestamp" }
|
|
595
|
+
},
|
|
596
|
+
required: ["roomId"]
|
|
597
|
+
}
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
name: "send_room_message",
|
|
601
|
+
description: "Send a message to a room.",
|
|
602
|
+
inputSchema: {
|
|
603
|
+
type: "object",
|
|
604
|
+
properties: {
|
|
605
|
+
roomId: { type: "string", description: "Room ID" },
|
|
606
|
+
content: { type: "string", description: "Message content" },
|
|
607
|
+
tempId: { type: "string", description: "Optional client temp ID" }
|
|
608
|
+
},
|
|
609
|
+
required: ["roomId", "content"]
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
name: "join_room",
|
|
614
|
+
description: "Join a room, optionally with an invite code.",
|
|
615
|
+
inputSchema: {
|
|
616
|
+
type: "object",
|
|
617
|
+
properties: {
|
|
618
|
+
roomId: { type: "string", description: "Room ID" },
|
|
619
|
+
inviteCode: { type: "string", description: "Optional invite code" }
|
|
620
|
+
},
|
|
621
|
+
required: ["roomId"]
|
|
622
|
+
}
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
name: "leave_room",
|
|
626
|
+
description: "Leave a room.",
|
|
627
|
+
inputSchema: {
|
|
628
|
+
type: "object",
|
|
629
|
+
properties: { roomId: { type: "string", description: "Room ID" } },
|
|
630
|
+
required: ["roomId"]
|
|
631
|
+
}
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
name: "remove_room_member",
|
|
635
|
+
description: "Remove a member from a room.",
|
|
636
|
+
inputSchema: {
|
|
637
|
+
type: "object",
|
|
638
|
+
properties: {
|
|
639
|
+
roomId: { type: "string", description: "Room ID" },
|
|
640
|
+
uid: { type: "string", description: "User ID to remove" }
|
|
641
|
+
},
|
|
642
|
+
required: ["roomId", "uid"]
|
|
643
|
+
}
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
name: "promote_room_member",
|
|
647
|
+
description: "Change a room member role.",
|
|
648
|
+
inputSchema: {
|
|
649
|
+
type: "object",
|
|
650
|
+
properties: {
|
|
651
|
+
roomId: { type: "string", description: "Room ID" },
|
|
652
|
+
uid: { type: "string", description: "User ID to promote" },
|
|
653
|
+
role: { type: "string", description: "Target role", enum: ["admin", "member", "owner"] }
|
|
654
|
+
},
|
|
655
|
+
required: ["roomId", "uid", "role"]
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
name: "create_room_invite",
|
|
660
|
+
description: "Create or refresh a room invite code.",
|
|
661
|
+
inputSchema: {
|
|
662
|
+
type: "object",
|
|
663
|
+
properties: {
|
|
664
|
+
roomId: { type: "string", description: "Room ID" },
|
|
665
|
+
expiresInHours: { type: "number", description: "Invite expiry in hours" },
|
|
666
|
+
maxUses: { type: "number", description: "Optional max uses" }
|
|
667
|
+
},
|
|
668
|
+
required: ["roomId"]
|
|
669
|
+
}
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
name: "list_room_pins",
|
|
673
|
+
description: "List pinned evidence items in a room.",
|
|
674
|
+
inputSchema: {
|
|
675
|
+
type: "object",
|
|
676
|
+
properties: { roomId: { type: "string", description: "Room ID" } },
|
|
677
|
+
required: ["roomId"]
|
|
678
|
+
}
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
name: "pin_room_message",
|
|
682
|
+
description: "Pin a room message as evidence.",
|
|
683
|
+
inputSchema: {
|
|
684
|
+
type: "object",
|
|
685
|
+
properties: {
|
|
686
|
+
roomId: { type: "string", description: "Room ID" },
|
|
687
|
+
messageId: { type: "string", description: "Message ID" },
|
|
688
|
+
category: { type: "string", description: "Pin category", default: "evidence" },
|
|
689
|
+
note: { type: "string", description: "Optional note" }
|
|
690
|
+
},
|
|
691
|
+
required: ["roomId", "messageId"]
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
name: "unpin_room_message",
|
|
696
|
+
description: "Remove a pinned message from the evidence board.",
|
|
697
|
+
inputSchema: {
|
|
698
|
+
type: "object",
|
|
699
|
+
properties: {
|
|
700
|
+
roomId: { type: "string", description: "Room ID" },
|
|
701
|
+
messageId: { type: "string", description: "Message ID" }
|
|
702
|
+
},
|
|
703
|
+
required: ["roomId", "messageId"]
|
|
704
|
+
}
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
name: "edit_room_message",
|
|
708
|
+
description: "Edit a room message.",
|
|
709
|
+
inputSchema: {
|
|
710
|
+
type: "object",
|
|
711
|
+
properties: {
|
|
712
|
+
roomId: { type: "string", description: "Room ID" },
|
|
713
|
+
messageId: { type: "string", description: "Message ID" },
|
|
714
|
+
content: { type: "string", description: "Updated content" }
|
|
715
|
+
},
|
|
716
|
+
required: ["roomId", "messageId", "content"]
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
name: "delete_room_message",
|
|
721
|
+
description: "Delete a room message.",
|
|
722
|
+
inputSchema: {
|
|
723
|
+
type: "object",
|
|
724
|
+
properties: {
|
|
725
|
+
roomId: { type: "string", description: "Room ID" },
|
|
726
|
+
messageId: { type: "string", description: "Message ID" }
|
|
727
|
+
},
|
|
728
|
+
required: ["roomId", "messageId"]
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
name: "export_room",
|
|
733
|
+
description: "Export a room as structured evidence data.",
|
|
734
|
+
inputSchema: {
|
|
735
|
+
type: "object",
|
|
736
|
+
properties: { roomId: { type: "string", description: "Room ID" } },
|
|
737
|
+
required: ["roomId"]
|
|
738
|
+
}
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
name: "get_polymarket_markets",
|
|
742
|
+
description: "List Polymarket markets with filters.",
|
|
743
|
+
inputSchema: {
|
|
744
|
+
type: "object",
|
|
745
|
+
properties: {
|
|
746
|
+
q: { type: "string", description: "Search query" },
|
|
747
|
+
active: { type: "boolean", description: "Only active markets" },
|
|
748
|
+
closed: { type: "boolean", description: "Only closed markets" },
|
|
749
|
+
limit: { type: "number", description: "Limit", default: 20 },
|
|
750
|
+
offset: { type: "number", description: "Offset", default: 0 },
|
|
751
|
+
order: { type: "string", description: "Sort order", enum: ["volume24hr", "liquidity", "endDate", "startDate"] }
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
name: "get_polymarket_market",
|
|
757
|
+
description: "Get a Polymarket market by slug.",
|
|
758
|
+
inputSchema: {
|
|
759
|
+
type: "object",
|
|
760
|
+
properties: { slug: { type: "string", description: "Market slug" } },
|
|
761
|
+
required: ["slug"]
|
|
762
|
+
}
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
name: "get_polymarket_trending",
|
|
766
|
+
description: "Get trending Polymarket markets.",
|
|
767
|
+
inputSchema: {
|
|
768
|
+
type: "object",
|
|
769
|
+
properties: { limit: { type: "number", description: "Limit", default: 10 } }
|
|
770
|
+
}
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
name: "get_polymarket_spikes",
|
|
774
|
+
description: "Detect Polymarket volume spikes.",
|
|
775
|
+
inputSchema: {
|
|
776
|
+
type: "object",
|
|
777
|
+
properties: {
|
|
778
|
+
threshold: { type: "number", description: "Spike threshold", default: 2 },
|
|
779
|
+
minVolume: { type: "number", description: "Minimum volume", default: 1e4 }
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
name: "get_polymarket_movers",
|
|
785
|
+
description: "Get Polymarket markets with large price movements.",
|
|
786
|
+
inputSchema: {
|
|
787
|
+
type: "object",
|
|
788
|
+
properties: { minChange: { type: "number", description: "Minimum percentage change", default: 0.05 } }
|
|
789
|
+
}
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
name: "get_polymarket_events",
|
|
793
|
+
description: "Get Polymarket events.",
|
|
794
|
+
inputSchema: {
|
|
795
|
+
type: "object",
|
|
796
|
+
properties: {
|
|
797
|
+
active: { type: "boolean", description: "Only active events" },
|
|
798
|
+
limit: { type: "number", description: "Limit", default: 20 },
|
|
799
|
+
offset: { type: "number", description: "Offset", default: 0 }
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
name: "get_polymarket_leaderboard",
|
|
805
|
+
description: "Get top Polymarket traders.",
|
|
806
|
+
inputSchema: {
|
|
807
|
+
type: "object",
|
|
808
|
+
properties: { limit: { type: "number", description: "Limit", default: 20 } }
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
{
|
|
812
|
+
name: "get_polymarket_trader",
|
|
813
|
+
description: "Get a Polymarket trader profile.",
|
|
814
|
+
inputSchema: {
|
|
815
|
+
type: "object",
|
|
816
|
+
properties: { address: { type: "string", description: "Trader address" } },
|
|
817
|
+
required: ["address"]
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
name: "get_polymarket_order_book",
|
|
822
|
+
description: "Get a Polymarket order book.",
|
|
823
|
+
inputSchema: {
|
|
824
|
+
type: "object",
|
|
825
|
+
properties: { tokenId: { type: "string", description: "Order book token ID" } },
|
|
826
|
+
required: ["tokenId"]
|
|
827
|
+
}
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
name: "get_polymarket_trades",
|
|
831
|
+
description: "Get Polymarket trades for a condition.",
|
|
832
|
+
inputSchema: {
|
|
833
|
+
type: "object",
|
|
834
|
+
properties: {
|
|
835
|
+
conditionId: { type: "string", description: "Condition ID" },
|
|
836
|
+
limit: { type: "number", description: "Limit", default: 20 }
|
|
837
|
+
},
|
|
838
|
+
required: ["conditionId"]
|
|
839
|
+
}
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
name: "get_polymarket_history",
|
|
843
|
+
description: "Get Polymarket price history.",
|
|
844
|
+
inputSchema: {
|
|
845
|
+
type: "object",
|
|
846
|
+
properties: {
|
|
847
|
+
conditionId: { type: "string", description: "Condition ID" },
|
|
848
|
+
interval: { type: "string", description: "History interval", enum: ["hour", "day"] },
|
|
849
|
+
limit: { type: "number", description: "Limit", default: 50 }
|
|
850
|
+
},
|
|
851
|
+
required: ["conditionId"]
|
|
852
|
+
}
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
name: "fetch_dune_interactors",
|
|
856
|
+
description: "Fetch contract interactors from Dune Analytics.",
|
|
857
|
+
inputSchema: {
|
|
858
|
+
type: "object",
|
|
859
|
+
properties: {
|
|
860
|
+
contractAddress: { type: "string", description: "Contract address" },
|
|
861
|
+
chain: { type: "string", description: "Chain name", enum: ["ethereum", "polygon", "arbitrum", "optimism", "base", "linea"] },
|
|
862
|
+
limit: { type: "number", description: "Row limit", default: 1e3 },
|
|
863
|
+
customApiKey: { type: "string", description: "Optional Dune API key override" }
|
|
864
|
+
},
|
|
865
|
+
required: ["contractAddress", "chain"]
|
|
866
|
+
}
|
|
219
867
|
}
|
|
220
|
-
|
|
868
|
+
];
|
|
869
|
+
|
|
870
|
+
// src/mcp/api-handlers.ts
|
|
871
|
+
import { default as axios } from "axios";
|
|
221
872
|
|
|
222
873
|
// src/mcp/mcpLogger.ts
|
|
874
|
+
var mcpLogWarnings = 0;
|
|
223
875
|
async function logMcpRequest(entry) {
|
|
876
|
+
if (process.env.FUNDTRACER_MCP_DISABLE_LOGGING === "1") {
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
224
879
|
try {
|
|
225
880
|
const { getFirestore } = await import("../firebase.js");
|
|
226
881
|
const db = getFirestore();
|
|
@@ -237,25 +892,181 @@ async function logMcpRequest(entry) {
|
|
|
237
892
|
}
|
|
238
893
|
}
|
|
239
894
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
895
|
+
|
|
896
|
+
// src/utils/nameResolver.ts
|
|
897
|
+
import { JsonRpcProvider, keccak256, toUtf8Bytes } from "ethers";
|
|
898
|
+
var ETH_ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
|
|
899
|
+
var BASENAME_RESOLVER = "0xC6d566A56A1aFf6508b41f6c90ff131615583BCD";
|
|
900
|
+
var BASE_RPC = "https://mainnet.base.org";
|
|
901
|
+
var LNS_REGISTRY = "0x50130b669B28C339991d8676FA73CF122a121267";
|
|
902
|
+
var LNS_ADDR_SELECTOR = "0x3b3b57de";
|
|
903
|
+
var cache = /* @__PURE__ */ new Map();
|
|
904
|
+
var CACHE_TTL = 5 * 60 * 1e3;
|
|
905
|
+
var MAX_CACHE_SIZE = 500;
|
|
906
|
+
function getFromCache(name) {
|
|
907
|
+
const key = name.toLowerCase();
|
|
908
|
+
const entry = cache.get(key);
|
|
909
|
+
if (!entry) return null;
|
|
910
|
+
if (Date.now() - entry.timestamp > CACHE_TTL) {
|
|
911
|
+
cache.delete(key);
|
|
912
|
+
return null;
|
|
244
913
|
}
|
|
245
|
-
|
|
914
|
+
cache.delete(key);
|
|
915
|
+
cache.set(key, entry);
|
|
916
|
+
return entry.address;
|
|
917
|
+
}
|
|
918
|
+
function setCache(name, address) {
|
|
919
|
+
if (cache.size >= MAX_CACHE_SIZE) {
|
|
920
|
+
const first = cache.keys().next().value;
|
|
921
|
+
if (first) cache.delete(first);
|
|
922
|
+
}
|
|
923
|
+
cache.set(name.toLowerCase(), { address, timestamp: Date.now() });
|
|
924
|
+
}
|
|
925
|
+
function isEnsName(input) {
|
|
926
|
+
if (!input || typeof input !== "string") return false;
|
|
927
|
+
if (ETH_ADDRESS_RE.test(input)) return false;
|
|
928
|
+
return /\.(eth|base\.eth|linea\.eth)$/i.test(input.trim());
|
|
929
|
+
}
|
|
930
|
+
function namehash(name) {
|
|
931
|
+
let node = "0000000000000000000000000000000000000000000000000000000000000000";
|
|
932
|
+
if (!name) return "0x" + node;
|
|
933
|
+
const labels = name.split(".");
|
|
934
|
+
for (let i = labels.length - 1; i >= 0; i--) {
|
|
935
|
+
const labelHash = keccak256(toUtf8Bytes(labels[i])).slice(2);
|
|
936
|
+
node = keccak256("0x" + node + labelHash).slice(2);
|
|
937
|
+
}
|
|
938
|
+
return "0x" + node;
|
|
939
|
+
}
|
|
940
|
+
async function resolveViaContract(rpcUrl, resolverAddress, name) {
|
|
941
|
+
try {
|
|
942
|
+
const node = namehash(name);
|
|
943
|
+
const res = await fetch(rpcUrl, {
|
|
944
|
+
method: "POST",
|
|
945
|
+
headers: { "Content-Type": "application/json" },
|
|
946
|
+
body: JSON.stringify({
|
|
947
|
+
jsonrpc: "2.0",
|
|
948
|
+
id: 1,
|
|
949
|
+
method: "eth_call",
|
|
950
|
+
params: [{
|
|
951
|
+
to: resolverAddress,
|
|
952
|
+
data: LNS_ADDR_SELECTOR + node.slice(2)
|
|
953
|
+
}, "latest"]
|
|
954
|
+
})
|
|
955
|
+
});
|
|
956
|
+
const data = await res.json();
|
|
957
|
+
const raw = data?.result;
|
|
958
|
+
if (!raw || raw === "0x" || raw === "0x" + "0".repeat(64)) return null;
|
|
959
|
+
return "0x" + raw.slice(26);
|
|
960
|
+
} catch {
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
async function resolveEns(name) {
|
|
965
|
+
const key = process.env.DEFAULT_ALCHEMY_API_KEY || process.env.ALCHEMY_API_KEY || "";
|
|
966
|
+
if (!key) {
|
|
967
|
+
console.error("[nameResolver] No Alchemy API key configured for ENS resolution");
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
const provider = new JsonRpcProvider(`https://eth-mainnet.g.alchemy.com/v2/${key}`);
|
|
971
|
+
try {
|
|
972
|
+
const resolved = await Promise.race([
|
|
973
|
+
provider.resolveName(name),
|
|
974
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("ENS resolution timed out after 8s")), 8e3))
|
|
975
|
+
]);
|
|
976
|
+
return resolved || null;
|
|
977
|
+
} catch (err2) {
|
|
978
|
+
console.error(`[nameResolver] ENS resolveName("${name}") failed:`, err2.message || err2);
|
|
979
|
+
return null;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
async function resolveBasename(name) {
|
|
983
|
+
try {
|
|
984
|
+
const ensResult = await resolveEns(name);
|
|
985
|
+
if (ensResult) return ensResult;
|
|
986
|
+
} catch {
|
|
987
|
+
}
|
|
988
|
+
return resolveViaContract(BASE_RPC, BASENAME_RESOLVER, name);
|
|
989
|
+
}
|
|
990
|
+
async function resolveLineaName(name) {
|
|
991
|
+
try {
|
|
992
|
+
const ensResult = await resolveEns(name);
|
|
993
|
+
if (ensResult) return ensResult;
|
|
994
|
+
} catch {
|
|
995
|
+
}
|
|
996
|
+
const rpcUrl = process.env.LINEA_RPC_URL || "https://rpc.linea.build";
|
|
997
|
+
return resolveViaContract(rpcUrl, LNS_REGISTRY, name);
|
|
998
|
+
}
|
|
999
|
+
async function tryResolveAddress(input) {
|
|
1000
|
+
if (!input || typeof input !== "string") {
|
|
1001
|
+
return { resolved: input || "", error: "Address is required" };
|
|
1002
|
+
}
|
|
1003
|
+
const trimmed = input.trim();
|
|
1004
|
+
if (ETH_ADDRESS_RE.test(trimmed)) {
|
|
1005
|
+
return { resolved: trimmed.toLowerCase() };
|
|
1006
|
+
}
|
|
1007
|
+
if (!isEnsName(trimmed)) {
|
|
1008
|
+
return { resolved: trimmed };
|
|
1009
|
+
}
|
|
1010
|
+
const cached = getFromCache(trimmed);
|
|
1011
|
+
if (cached) return { resolved: cached };
|
|
1012
|
+
const lower = trimmed.toLowerCase();
|
|
1013
|
+
let resolved = null;
|
|
1014
|
+
try {
|
|
1015
|
+
if (lower.endsWith(".base.eth")) {
|
|
1016
|
+
resolved = await resolveBasename(trimmed);
|
|
1017
|
+
} else if (lower.endsWith(".linea.eth")) {
|
|
1018
|
+
resolved = await resolveLineaName(trimmed);
|
|
1019
|
+
} else {
|
|
1020
|
+
resolved = await resolveEns(trimmed);
|
|
1021
|
+
}
|
|
1022
|
+
} catch {
|
|
1023
|
+
resolved = null;
|
|
1024
|
+
}
|
|
1025
|
+
if (!resolved) {
|
|
1026
|
+
const hints = {
|
|
1027
|
+
".base.eth": "Basename may not be registered on Base. Register at base.org/names.",
|
|
1028
|
+
".linea.eth": "Linea name may not be registered. Register via Linea Name Service.",
|
|
1029
|
+
".eth": "ENS name may not be registered. Check at app.ens.domains."
|
|
1030
|
+
};
|
|
1031
|
+
const hint = hints[lower.match(/\.(?:base\.eth|linea\.eth|eth)$/)?.[0] || ""] || "ENS name may not be registered or is not resolvable.";
|
|
1032
|
+
return {
|
|
1033
|
+
resolved: trimmed,
|
|
1034
|
+
error: `Could not resolve "${trimmed}": ${hint}`
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
setCache(trimmed, resolved);
|
|
1038
|
+
return { resolved };
|
|
1039
|
+
}
|
|
246
1040
|
|
|
247
1041
|
// src/mcp/api-handlers.ts
|
|
248
|
-
var api_handlers_exports = {};
|
|
249
|
-
__export(api_handlers_exports, {
|
|
250
|
-
TOOL_HANDLERS: () => TOOL_HANDLERS
|
|
251
|
-
});
|
|
252
|
-
import { default as axios } from "axios";
|
|
253
1042
|
function ok(text) {
|
|
254
1043
|
return { content: [{ type: "text", text }] };
|
|
255
1044
|
}
|
|
256
1045
|
function err(message) {
|
|
257
1046
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
258
1047
|
}
|
|
1048
|
+
function extractApiError(error) {
|
|
1049
|
+
return error?.response?.data?.error || error?.response?.data?.message || error?.message || "Unknown error";
|
|
1050
|
+
}
|
|
1051
|
+
async function resolveAddressInput(input) {
|
|
1052
|
+
const { resolved, error } = await tryResolveAddress(input);
|
|
1053
|
+
if (error) return { error: err(error) };
|
|
1054
|
+
return { resolved };
|
|
1055
|
+
}
|
|
1056
|
+
async function routeGet(path, params) {
|
|
1057
|
+
return api().get(path, { params });
|
|
1058
|
+
}
|
|
1059
|
+
async function routePost(path, body, params) {
|
|
1060
|
+
return api().post(path, body, { params });
|
|
1061
|
+
}
|
|
1062
|
+
async function routePatch(path, body, params) {
|
|
1063
|
+
return api().patch(path, body, { params });
|
|
1064
|
+
}
|
|
1065
|
+
async function routeDelete(path, params) {
|
|
1066
|
+
return api().delete(path, { params });
|
|
1067
|
+
}
|
|
1068
|
+
var API_BASE = process.env.FUNDTRACER_API_URL || "https://api.fundtracer.xyz";
|
|
1069
|
+
var _mcpCtx = null;
|
|
259
1070
|
function api() {
|
|
260
1071
|
const key = _mcpCtx?.apiKey || process.env.FUNDTRACER_MCP_API_KEY || "";
|
|
261
1072
|
const headers = {
|
|
@@ -272,6 +1083,782 @@ function api() {
|
|
|
272
1083
|
headers
|
|
273
1084
|
});
|
|
274
1085
|
}
|
|
1086
|
+
var analyzeWallet = async (args, ctx) => {
|
|
1087
|
+
const { address, chainId, transactionLimit } = args;
|
|
1088
|
+
try {
|
|
1089
|
+
const res = await api().post("/api/analyze/wallet", {
|
|
1090
|
+
address,
|
|
1091
|
+
chain: chainId,
|
|
1092
|
+
options: { limit: transactionLimit || 500 }
|
|
1093
|
+
});
|
|
1094
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
const msg = error.response?.data?.error || error.message;
|
|
1097
|
+
return err(`Wallet analysis failed: ${msg}`);
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
var traceFunds = async (args, ctx) => {
|
|
1101
|
+
const { address, chainId, maxDepth = 3, direction = "both" } = args;
|
|
1102
|
+
try {
|
|
1103
|
+
const res = await api().post("/api/analyze/funding-tree", {
|
|
1104
|
+
address,
|
|
1105
|
+
chain: chainId,
|
|
1106
|
+
maxDepth,
|
|
1107
|
+
direction
|
|
1108
|
+
});
|
|
1109
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1110
|
+
} catch (error) {
|
|
1111
|
+
const msg = error.response?.data?.error || error.message;
|
|
1112
|
+
return err(`Fund tracing failed: ${msg}`);
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
var compareWallets = async (args, ctx) => {
|
|
1116
|
+
const { addresses, chainId } = args;
|
|
1117
|
+
const addrList = addresses.split(",").map((a) => a.trim()).filter(Boolean);
|
|
1118
|
+
if (addrList.length < 2) return err("At least 2 addresses required");
|
|
1119
|
+
try {
|
|
1120
|
+
const res = await api().post("/api/analyze/compare", {
|
|
1121
|
+
addresses: addrList,
|
|
1122
|
+
chain: chainId
|
|
1123
|
+
});
|
|
1124
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1125
|
+
} catch (error) {
|
|
1126
|
+
const msg = error.response?.data?.error || error.message;
|
|
1127
|
+
return err(`Wallet comparison failed: ${msg}`);
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
var analyzeContract = async (args, ctx) => {
|
|
1131
|
+
const { contractAddress, chainId, maxInteractors = 100 } = args;
|
|
1132
|
+
try {
|
|
1133
|
+
const res = await api().post("/api/analyze/contract", {
|
|
1134
|
+
contractAddress,
|
|
1135
|
+
chain: chainId,
|
|
1136
|
+
maxInteractors
|
|
1137
|
+
});
|
|
1138
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1139
|
+
} catch (error) {
|
|
1140
|
+
const msg = error.response?.data?.error || error.message;
|
|
1141
|
+
return err(`Contract analysis failed: ${msg}`);
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
var detectSybilClusters = async (args, ctx) => {
|
|
1145
|
+
const { addresses, chainId } = args;
|
|
1146
|
+
const addrList = addresses.split(",").map((a) => a.trim()).filter(Boolean);
|
|
1147
|
+
if (addrList.length < 3) return err("At least 3 addresses required for cluster detection");
|
|
1148
|
+
try {
|
|
1149
|
+
const res = await api().post("/api/analyze/sybil", {
|
|
1150
|
+
addresses: addrList,
|
|
1151
|
+
chain: chainId
|
|
1152
|
+
});
|
|
1153
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1154
|
+
} catch (error) {
|
|
1155
|
+
const msg = error.response?.data?.error || error.message;
|
|
1156
|
+
return err(`Sybil detection failed: ${msg}`);
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
var getPortfolio = async (args, ctx) => {
|
|
1160
|
+
const { address, chainId } = args;
|
|
1161
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(address);
|
|
1162
|
+
if (resolveErr) return resolveErr;
|
|
1163
|
+
try {
|
|
1164
|
+
const res = await api().get(`/api/portfolio/${encodeURIComponent(resolvedAddr || address)}`, {
|
|
1165
|
+
params: { chain: chainId }
|
|
1166
|
+
});
|
|
1167
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1168
|
+
} catch (error) {
|
|
1169
|
+
const msg = extractApiError(error);
|
|
1170
|
+
return err(`Portfolio fetch failed: ${msg}`);
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
var getTransactions = async (args, ctx) => {
|
|
1174
|
+
const { address, chainId, limit = 50 } = args;
|
|
1175
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(address);
|
|
1176
|
+
if (resolveErr) return resolveErr;
|
|
1177
|
+
try {
|
|
1178
|
+
const res = await api().post("/api/history", {
|
|
1179
|
+
wallet: resolvedAddr || address,
|
|
1180
|
+
blockchain: chainId,
|
|
1181
|
+
pageToken: null,
|
|
1182
|
+
filters: {}
|
|
1183
|
+
});
|
|
1184
|
+
const txs = (res.data.transactions || []).slice(0, limit);
|
|
1185
|
+
return ok(JSON.stringify({
|
|
1186
|
+
address,
|
|
1187
|
+
chainId,
|
|
1188
|
+
transactions: txs,
|
|
1189
|
+
totalCount: res.data.transactions?.length || 0
|
|
1190
|
+
}, null, 2));
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
const msg = extractApiError(error);
|
|
1193
|
+
return err(`Transaction fetch failed: ${msg}`);
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
var lookupEntity = async (args, ctx) => {
|
|
1197
|
+
const { query, chainId } = args;
|
|
1198
|
+
const chain = chainId || "ethereum";
|
|
1199
|
+
const { resolved: resolvedQuery } = await resolveAddressInput(query);
|
|
1200
|
+
const lookupValue = resolvedQuery || query;
|
|
1201
|
+
try {
|
|
1202
|
+
if (/^0x[a-fA-F0-9]{40}$/.test(lookupValue) || /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(lookupValue)) {
|
|
1203
|
+
const res2 = await api().get(`/api/entities/${lookupValue}`, {
|
|
1204
|
+
params: { chain }
|
|
1205
|
+
});
|
|
1206
|
+
return ok(JSON.stringify(res2.data, null, 2));
|
|
1207
|
+
}
|
|
1208
|
+
const res = await api().get("/api/entities/search", {
|
|
1209
|
+
params: { q: lookupValue, chain }
|
|
1210
|
+
});
|
|
1211
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1212
|
+
} catch (error) {
|
|
1213
|
+
if (error.response?.status === 404) {
|
|
1214
|
+
return ok(JSON.stringify({ query: lookupValue, label: "Unknown address", chain }, null, 2));
|
|
1215
|
+
}
|
|
1216
|
+
const msg = error.response?.data?.error || error.message;
|
|
1217
|
+
return err(`Entity lookup failed: ${msg}`);
|
|
1218
|
+
}
|
|
1219
|
+
};
|
|
1220
|
+
var getGasPrices = async (args, ctx) => {
|
|
1221
|
+
const { chainId } = args;
|
|
1222
|
+
try {
|
|
1223
|
+
const res = await api().get("/api/gas", {
|
|
1224
|
+
params: chainId ? { chain: chainId } : {}
|
|
1225
|
+
});
|
|
1226
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1227
|
+
} catch (error) {
|
|
1228
|
+
const msg = error.response?.data?.error || error.message;
|
|
1229
|
+
return err(`Gas price fetch failed: ${msg}`);
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
var getTokenInfo = async (args, ctx) => {
|
|
1233
|
+
const { tokenAddress, chainId } = args;
|
|
1234
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(tokenAddress);
|
|
1235
|
+
if (resolveErr) return resolveErr;
|
|
1236
|
+
try {
|
|
1237
|
+
const res = await routeGet(`/api/tokens/${encodeURIComponent(resolvedAddr || tokenAddress)}`, {
|
|
1238
|
+
chain: chainId
|
|
1239
|
+
});
|
|
1240
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
const msg = extractApiError(error);
|
|
1243
|
+
return err(`Token info fetch failed: ${msg}`);
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
var resolveName = async (args, ctx) => {
|
|
1247
|
+
const { input } = args;
|
|
1248
|
+
const { resolved, error } = await tryResolveAddress(input);
|
|
1249
|
+
const normalizedInput = typeof input === "string" ? input.trim().toLowerCase() : "";
|
|
1250
|
+
const normalizedResolved = typeof resolved === "string" ? resolved.toLowerCase() : "";
|
|
1251
|
+
return ok(JSON.stringify({
|
|
1252
|
+
input,
|
|
1253
|
+
resolved,
|
|
1254
|
+
isAddress: /^0x[a-fA-F0-9]{40}$/.test(resolved),
|
|
1255
|
+
isResolvedName: normalizedResolved !== normalizedInput,
|
|
1256
|
+
...error ? { warning: error } : {}
|
|
1257
|
+
}, null, 2));
|
|
1258
|
+
};
|
|
1259
|
+
var getTransactionDetail = async (args, ctx) => {
|
|
1260
|
+
const { chainId, hash } = args;
|
|
1261
|
+
try {
|
|
1262
|
+
const res = await routeGet(`/api/tx/${encodeURIComponent(chainId)}/${encodeURIComponent(hash)}`);
|
|
1263
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
return err(`Transaction detail fetch failed: ${extractApiError(error)}`);
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
var getTransactionHistory = async (args, ctx) => {
|
|
1269
|
+
const { address, chainId, pageToken = null, filters = {} } = args;
|
|
1270
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(address);
|
|
1271
|
+
if (resolveErr) return resolveErr;
|
|
1272
|
+
try {
|
|
1273
|
+
const res = await routePost("/api/history", {
|
|
1274
|
+
wallet: resolvedAddr || address,
|
|
1275
|
+
blockchain: chainId,
|
|
1276
|
+
pageToken,
|
|
1277
|
+
filters
|
|
1278
|
+
});
|
|
1279
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
return err(`Transaction history fetch failed: ${extractApiError(error)}`);
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
var analyzeCEXFlow = async (args, ctx) => {
|
|
1285
|
+
const { walletAddress, chainId, cexName, depth } = args;
|
|
1286
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(walletAddress);
|
|
1287
|
+
if (resolveErr) return resolveErr;
|
|
1288
|
+
try {
|
|
1289
|
+
const res = await routePost("/api/analyze/cex-flow", {
|
|
1290
|
+
walletAddress: resolvedAddr || walletAddress,
|
|
1291
|
+
chain: chainId,
|
|
1292
|
+
...cexName ? { cexName } : {},
|
|
1293
|
+
...depth !== void 0 ? { depth } : {}
|
|
1294
|
+
});
|
|
1295
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1296
|
+
} catch (error) {
|
|
1297
|
+
return err(`CEX flow analysis failed: ${extractApiError(error)}`);
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
var analyzeSybilAddresses = async (args, ctx) => {
|
|
1301
|
+
const { addresses, chainId, txHash } = args;
|
|
1302
|
+
const list = addresses.split(",").map((a) => a.trim()).filter(Boolean);
|
|
1303
|
+
const resolved = [];
|
|
1304
|
+
for (const a of list) {
|
|
1305
|
+
const r = await resolveAddressInput(a);
|
|
1306
|
+
if (r.error) return r.error;
|
|
1307
|
+
resolved.push(r.resolved || a);
|
|
1308
|
+
}
|
|
1309
|
+
try {
|
|
1310
|
+
const res = await routePost("/api/analyze/sybil-addresses", {
|
|
1311
|
+
addresses: resolved,
|
|
1312
|
+
chain: chainId,
|
|
1313
|
+
...txHash ? { txHash } : {}
|
|
1314
|
+
});
|
|
1315
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
return err(`Sybil address analysis failed: ${extractApiError(error)}`);
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
1320
|
+
var searchContracts = async (args) => {
|
|
1321
|
+
const { query } = args;
|
|
1322
|
+
try {
|
|
1323
|
+
const res = await routeGet("/api/contracts/search", { q: query });
|
|
1324
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
return err(`Contract search failed: ${extractApiError(error)}`);
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
var lookupContract = async (args) => {
|
|
1330
|
+
const { address } = args;
|
|
1331
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(address);
|
|
1332
|
+
if (resolveErr) return resolveErr;
|
|
1333
|
+
try {
|
|
1334
|
+
const res = await routeGet(`/api/contracts/lookup/${encodeURIComponent(resolvedAddr || address)}`);
|
|
1335
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
return err(`Contract lookup failed: ${extractApiError(error)}`);
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
var getContractStats = async () => {
|
|
1341
|
+
try {
|
|
1342
|
+
const res = await routeGet("/api/contracts/stats");
|
|
1343
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1344
|
+
} catch (error) {
|
|
1345
|
+
return err(`Contract stats fetch failed: ${extractApiError(error)}`);
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
var searchTokens = async (args) => {
|
|
1349
|
+
const { query } = args;
|
|
1350
|
+
try {
|
|
1351
|
+
const res = await routeGet("/api/tokens/search", { q: query });
|
|
1352
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1353
|
+
} catch (error) {
|
|
1354
|
+
return err(`Token search failed: ${extractApiError(error)}`);
|
|
1355
|
+
}
|
|
1356
|
+
};
|
|
1357
|
+
var getMarketStats = async () => {
|
|
1358
|
+
try {
|
|
1359
|
+
const res = await routeGet("/api/market/stats");
|
|
1360
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
return err(`Market stats fetch failed: ${extractApiError(error)}`);
|
|
1363
|
+
}
|
|
1364
|
+
};
|
|
1365
|
+
var getMarketCoins = async (args) => {
|
|
1366
|
+
const { chainId, page = 1, perPage = 100 } = args;
|
|
1367
|
+
try {
|
|
1368
|
+
const res = await routeGet("/api/market/coins", {
|
|
1369
|
+
chain: chainId || "all",
|
|
1370
|
+
page,
|
|
1371
|
+
per_page: perPage
|
|
1372
|
+
});
|
|
1373
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1374
|
+
} catch (error) {
|
|
1375
|
+
return err(`Market coins fetch failed: ${extractApiError(error)}`);
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
var getTokenChart = async (args) => {
|
|
1379
|
+
const { tokenAddress, chainId, coinId, days = 7 } = args;
|
|
1380
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(tokenAddress);
|
|
1381
|
+
if (resolveErr) return resolveErr;
|
|
1382
|
+
try {
|
|
1383
|
+
const res = await routeGet(`/api/tokens/${encodeURIComponent(resolvedAddr || tokenAddress)}/chart`, {
|
|
1384
|
+
chain: chainId,
|
|
1385
|
+
coinId,
|
|
1386
|
+
days
|
|
1387
|
+
});
|
|
1388
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
return err(`Token chart fetch failed: ${extractApiError(error)}`);
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
var getDexScreenerLatestProfiles = async () => {
|
|
1394
|
+
try {
|
|
1395
|
+
const res = await routeGet("/api/dexscreener/profiles/latest");
|
|
1396
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1397
|
+
} catch (error) {
|
|
1398
|
+
return err(`DEX Screener profiles fetch failed: ${extractApiError(error)}`);
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
var getDexScreenerBoosts = async () => {
|
|
1402
|
+
try {
|
|
1403
|
+
const res = await routeGet("/api/dexscreener/boosts/top");
|
|
1404
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1405
|
+
} catch (error) {
|
|
1406
|
+
return err(`DEX Screener boosts fetch failed: ${extractApiError(error)}`);
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
var searchDexScreenerPairs = async (args) => {
|
|
1410
|
+
const { query } = args;
|
|
1411
|
+
try {
|
|
1412
|
+
const res = await routeGet("/api/dexscreener/search", { q: query });
|
|
1413
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1414
|
+
} catch (error) {
|
|
1415
|
+
return err(`DEX Screener search failed: ${extractApiError(error)}`);
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
var getDexScreenerTokenDetails = async (args) => {
|
|
1419
|
+
const { chainId, tokenAddress } = args;
|
|
1420
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(tokenAddress);
|
|
1421
|
+
if (resolveErr) return resolveErr;
|
|
1422
|
+
try {
|
|
1423
|
+
const res = await routeGet(`/api/dexscreener/token/${encodeURIComponent(chainId)}/${encodeURIComponent(resolvedAddr || tokenAddress)}`);
|
|
1424
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1425
|
+
} catch (error) {
|
|
1426
|
+
return err(`DEX Screener token fetch failed: ${extractApiError(error)}`);
|
|
1427
|
+
}
|
|
1428
|
+
};
|
|
1429
|
+
var getDexScreenerTokenPairs = async (args) => {
|
|
1430
|
+
const { chainId, tokenAddress } = args;
|
|
1431
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(tokenAddress);
|
|
1432
|
+
if (resolveErr) return resolveErr;
|
|
1433
|
+
try {
|
|
1434
|
+
const res = await routeGet(`/api/dexscreener/pairs/${encodeURIComponent(chainId)}/${encodeURIComponent(resolvedAddr || tokenAddress)}`);
|
|
1435
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1436
|
+
} catch (error) {
|
|
1437
|
+
return err(`DEX Screener pairs fetch failed: ${extractApiError(error)}`);
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1440
|
+
var getDexScreenerTrending = async () => {
|
|
1441
|
+
try {
|
|
1442
|
+
const res = await routeGet("/api/dexscreener/trending");
|
|
1443
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1444
|
+
} catch (error) {
|
|
1445
|
+
return err(`DEX Screener trending fetch failed: ${extractApiError(error)}`);
|
|
1446
|
+
}
|
|
1447
|
+
};
|
|
1448
|
+
var getScanHistory = async () => {
|
|
1449
|
+
try {
|
|
1450
|
+
const res = await routeGet("/api/scan-history");
|
|
1451
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
return err(`Scan history fetch failed: ${extractApiError(error)}`);
|
|
1454
|
+
}
|
|
1455
|
+
};
|
|
1456
|
+
var getScanHistoryStats = async () => {
|
|
1457
|
+
try {
|
|
1458
|
+
const res = await routeGet("/api/scan-history/stats");
|
|
1459
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1460
|
+
} catch (error) {
|
|
1461
|
+
return err(`Scan history stats fetch failed: ${extractApiError(error)}`);
|
|
1462
|
+
}
|
|
1463
|
+
};
|
|
1464
|
+
var saveScanHistoryItem = async (args) => {
|
|
1465
|
+
try {
|
|
1466
|
+
const res = await routePost("/api/scan-history", args);
|
|
1467
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1468
|
+
} catch (error) {
|
|
1469
|
+
return err(`Scan history save failed: ${extractApiError(error)}`);
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
var syncScanHistory = async (args) => {
|
|
1473
|
+
const { items } = args;
|
|
1474
|
+
try {
|
|
1475
|
+
const res = await routePost("/api/scan-history/sync", { items });
|
|
1476
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1477
|
+
} catch (error) {
|
|
1478
|
+
return err(`Scan history sync failed: ${extractApiError(error)}`);
|
|
1479
|
+
}
|
|
1480
|
+
};
|
|
1481
|
+
var deleteScanHistoryItem = async (args) => {
|
|
1482
|
+
const { address } = args;
|
|
1483
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(address);
|
|
1484
|
+
if (resolveErr) return resolveErr;
|
|
1485
|
+
try {
|
|
1486
|
+
const res = await routeDelete(`/api/scan-history/${encodeURIComponent(resolvedAddr || address)}`);
|
|
1487
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1488
|
+
} catch (error) {
|
|
1489
|
+
return err(`Scan history delete failed: ${extractApiError(error)}`);
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
var clearScanHistory = async () => {
|
|
1493
|
+
try {
|
|
1494
|
+
const res = await routeDelete("/api/scan-history");
|
|
1495
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1496
|
+
} catch (error) {
|
|
1497
|
+
return err(`Scan history clear failed: ${extractApiError(error)}`);
|
|
1498
|
+
}
|
|
1499
|
+
};
|
|
1500
|
+
var listRooms = async () => {
|
|
1501
|
+
try {
|
|
1502
|
+
const res = await routeGet("/api/rooms");
|
|
1503
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1504
|
+
} catch (error) {
|
|
1505
|
+
return err(`Room listing failed: ${extractApiError(error)}`);
|
|
1506
|
+
}
|
|
1507
|
+
};
|
|
1508
|
+
var getRoomDetails = async (args) => {
|
|
1509
|
+
const { roomId } = args;
|
|
1510
|
+
try {
|
|
1511
|
+
const res = await routeGet(`/api/rooms/${encodeURIComponent(roomId)}`);
|
|
1512
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1513
|
+
} catch (error) {
|
|
1514
|
+
return err(`Room details fetch failed: ${extractApiError(error)}`);
|
|
1515
|
+
}
|
|
1516
|
+
};
|
|
1517
|
+
var createRoom = async (args) => {
|
|
1518
|
+
try {
|
|
1519
|
+
const res = await routePost("/api/rooms", args);
|
|
1520
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1521
|
+
} catch (error) {
|
|
1522
|
+
return err(`Room creation failed: ${extractApiError(error)}`);
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
var updateRoom = async (args) => {
|
|
1526
|
+
const { roomId, ...body } = args;
|
|
1527
|
+
try {
|
|
1528
|
+
const res = await routePatch(`/api/rooms/${encodeURIComponent(roomId)}`, body);
|
|
1529
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1530
|
+
} catch (error) {
|
|
1531
|
+
return err(`Room update failed: ${extractApiError(error)}`);
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1534
|
+
var deleteRoom = async (args) => {
|
|
1535
|
+
const { roomId } = args;
|
|
1536
|
+
try {
|
|
1537
|
+
const res = await routeDelete(`/api/rooms/${encodeURIComponent(roomId)}`);
|
|
1538
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1539
|
+
} catch (error) {
|
|
1540
|
+
return err(`Room delete failed: ${extractApiError(error)}`);
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
var listRoomMessages = async (args) => {
|
|
1544
|
+
const { roomId, limit = 50, before } = args;
|
|
1545
|
+
try {
|
|
1546
|
+
const params = { limit };
|
|
1547
|
+
if (before !== void 0) params.before = before;
|
|
1548
|
+
const res = await routeGet(`/api/rooms/${encodeURIComponent(roomId)}/messages`, params);
|
|
1549
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1550
|
+
} catch (error) {
|
|
1551
|
+
return err(`Room messages fetch failed: ${extractApiError(error)}`);
|
|
1552
|
+
}
|
|
1553
|
+
};
|
|
1554
|
+
var sendRoomMessage = async (args) => {
|
|
1555
|
+
const { roomId, content, tempId } = args;
|
|
1556
|
+
try {
|
|
1557
|
+
const res = await routePost(`/api/rooms/${encodeURIComponent(roomId)}/messages`, { content, tempId });
|
|
1558
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1559
|
+
} catch (error) {
|
|
1560
|
+
return err(`Room message send failed: ${extractApiError(error)}`);
|
|
1561
|
+
}
|
|
1562
|
+
};
|
|
1563
|
+
var joinRoom = async (args) => {
|
|
1564
|
+
const { roomId, inviteCode } = args;
|
|
1565
|
+
try {
|
|
1566
|
+
const res = await routePost(`/api/rooms/${encodeURIComponent(roomId)}/join`, inviteCode ? { inviteCode } : {});
|
|
1567
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1568
|
+
} catch (error) {
|
|
1569
|
+
return err(`Room join failed: ${extractApiError(error)}`);
|
|
1570
|
+
}
|
|
1571
|
+
};
|
|
1572
|
+
var leaveRoom = async (args) => {
|
|
1573
|
+
const { roomId } = args;
|
|
1574
|
+
try {
|
|
1575
|
+
const res = await routePost(`/api/rooms/${encodeURIComponent(roomId)}/leave`, {});
|
|
1576
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1577
|
+
} catch (error) {
|
|
1578
|
+
return err(`Room leave failed: ${extractApiError(error)}`);
|
|
1579
|
+
}
|
|
1580
|
+
};
|
|
1581
|
+
var removeRoomMember = async (args) => {
|
|
1582
|
+
const { roomId, uid } = args;
|
|
1583
|
+
try {
|
|
1584
|
+
const res = await routeDelete(`/api/rooms/${encodeURIComponent(roomId)}/members/${encodeURIComponent(uid)}`);
|
|
1585
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1586
|
+
} catch (error) {
|
|
1587
|
+
return err(`Room member removal failed: ${extractApiError(error)}`);
|
|
1588
|
+
}
|
|
1589
|
+
};
|
|
1590
|
+
var promoteRoomMember = async (args) => {
|
|
1591
|
+
const { roomId, uid, role } = args;
|
|
1592
|
+
try {
|
|
1593
|
+
const res = await routePost(`/api/rooms/${encodeURIComponent(roomId)}/members/${encodeURIComponent(uid)}/role`, { role });
|
|
1594
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1595
|
+
} catch (error) {
|
|
1596
|
+
return err(`Room member promotion failed: ${extractApiError(error)}`);
|
|
1597
|
+
}
|
|
1598
|
+
};
|
|
1599
|
+
var createRoomInvite = async (args) => {
|
|
1600
|
+
const { roomId, expiresInHours, maxUses } = args;
|
|
1601
|
+
try {
|
|
1602
|
+
const body = {};
|
|
1603
|
+
if (expiresInHours !== void 0) body.expiresInHours = expiresInHours;
|
|
1604
|
+
if (maxUses !== void 0) body.maxUses = maxUses;
|
|
1605
|
+
const res = await routePost(`/api/rooms/${encodeURIComponent(roomId)}/invite`, body);
|
|
1606
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1607
|
+
} catch (error) {
|
|
1608
|
+
return err(`Room invite creation failed: ${extractApiError(error)}`);
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
var listRoomPins = async (args) => {
|
|
1612
|
+
const { roomId } = args;
|
|
1613
|
+
try {
|
|
1614
|
+
const res = await routeGet(`/api/rooms/${encodeURIComponent(roomId)}/pins`);
|
|
1615
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1616
|
+
} catch (error) {
|
|
1617
|
+
return err(`Room pins fetch failed: ${extractApiError(error)}`);
|
|
1618
|
+
}
|
|
1619
|
+
};
|
|
1620
|
+
var pinRoomMessage = async (args) => {
|
|
1621
|
+
const { roomId, messageId, category, note } = args;
|
|
1622
|
+
try {
|
|
1623
|
+
const res = await routePost(`/api/rooms/${encodeURIComponent(roomId)}/messages/${encodeURIComponent(messageId)}/pin`, { category, note });
|
|
1624
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1625
|
+
} catch (error) {
|
|
1626
|
+
return err(`Room pin failed: ${extractApiError(error)}`);
|
|
1627
|
+
}
|
|
1628
|
+
};
|
|
1629
|
+
var unpinRoomMessage = async (args) => {
|
|
1630
|
+
const { roomId, messageId } = args;
|
|
1631
|
+
try {
|
|
1632
|
+
const res = await routeDelete(`/api/rooms/${encodeURIComponent(roomId)}/messages/${encodeURIComponent(messageId)}/pin`);
|
|
1633
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1634
|
+
} catch (error) {
|
|
1635
|
+
return err(`Room unpin failed: ${extractApiError(error)}`);
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
var editRoomMessage = async (args) => {
|
|
1639
|
+
const { roomId, messageId, content } = args;
|
|
1640
|
+
try {
|
|
1641
|
+
const res = await routePatch(`/api/rooms/${encodeURIComponent(roomId)}/messages/${encodeURIComponent(messageId)}`, { content });
|
|
1642
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1643
|
+
} catch (error) {
|
|
1644
|
+
return err(`Room message edit failed: ${extractApiError(error)}`);
|
|
1645
|
+
}
|
|
1646
|
+
};
|
|
1647
|
+
var deleteRoomMessage = async (args) => {
|
|
1648
|
+
const { roomId, messageId } = args;
|
|
1649
|
+
try {
|
|
1650
|
+
const res = await routeDelete(`/api/rooms/${encodeURIComponent(roomId)}/messages/${encodeURIComponent(messageId)}`);
|
|
1651
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1652
|
+
} catch (error) {
|
|
1653
|
+
return err(`Room message delete failed: ${extractApiError(error)}`);
|
|
1654
|
+
}
|
|
1655
|
+
};
|
|
1656
|
+
var exportRoom = async (args) => {
|
|
1657
|
+
const { roomId } = args;
|
|
1658
|
+
try {
|
|
1659
|
+
const res = await routeGet(`/api/rooms/${encodeURIComponent(roomId)}/export`);
|
|
1660
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1661
|
+
} catch (error) {
|
|
1662
|
+
return err(`Room export failed: ${extractApiError(error)}`);
|
|
1663
|
+
}
|
|
1664
|
+
};
|
|
1665
|
+
var getPolymarketMarkets = async (args) => {
|
|
1666
|
+
const options = args;
|
|
1667
|
+
try {
|
|
1668
|
+
const res = await routeGet("/api/polymarket/markets", options);
|
|
1669
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1670
|
+
} catch (error) {
|
|
1671
|
+
return err(`Polymarket markets fetch failed: ${extractApiError(error)}`);
|
|
1672
|
+
}
|
|
1673
|
+
};
|
|
1674
|
+
var getPolymarketMarket = async (args) => {
|
|
1675
|
+
const { slug } = args;
|
|
1676
|
+
try {
|
|
1677
|
+
const res = await routeGet(`/api/polymarket/markets/${encodeURIComponent(slug)}`);
|
|
1678
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1679
|
+
} catch (error) {
|
|
1680
|
+
return err(`Polymarket market fetch failed: ${extractApiError(error)}`);
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1683
|
+
var getPolymarketTrending = async (args) => {
|
|
1684
|
+
const { limit } = args;
|
|
1685
|
+
try {
|
|
1686
|
+
const res = await routeGet("/api/polymarket/trending", limit ? { limit } : void 0);
|
|
1687
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1688
|
+
} catch (error) {
|
|
1689
|
+
return err(`Polymarket trending fetch failed: ${extractApiError(error)}`);
|
|
1690
|
+
}
|
|
1691
|
+
};
|
|
1692
|
+
var getPolymarketSpikes = async (args) => {
|
|
1693
|
+
const { threshold, minVolume } = args;
|
|
1694
|
+
try {
|
|
1695
|
+
const params = {};
|
|
1696
|
+
if (threshold !== void 0) params.threshold = threshold;
|
|
1697
|
+
if (minVolume !== void 0) params.minVolume = minVolume;
|
|
1698
|
+
const res = await routeGet("/api/polymarket/spikes", params);
|
|
1699
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1700
|
+
} catch (error) {
|
|
1701
|
+
return err(`Polymarket spikes fetch failed: ${extractApiError(error)}`);
|
|
1702
|
+
}
|
|
1703
|
+
};
|
|
1704
|
+
var getPolymarketMovers = async (args) => {
|
|
1705
|
+
const { minChange } = args;
|
|
1706
|
+
try {
|
|
1707
|
+
const res = await routeGet("/api/polymarket/movers", minChange ? { minChange } : void 0);
|
|
1708
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1709
|
+
} catch (error) {
|
|
1710
|
+
return err(`Polymarket movers fetch failed: ${extractApiError(error)}`);
|
|
1711
|
+
}
|
|
1712
|
+
};
|
|
1713
|
+
var getPolymarketEvents = async (args) => {
|
|
1714
|
+
const options = args;
|
|
1715
|
+
try {
|
|
1716
|
+
const res = await routeGet("/api/polymarket/events", options);
|
|
1717
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1718
|
+
} catch (error) {
|
|
1719
|
+
return err(`Polymarket events fetch failed: ${extractApiError(error)}`);
|
|
1720
|
+
}
|
|
1721
|
+
};
|
|
1722
|
+
var getPolymarketLeaderboard = async (args) => {
|
|
1723
|
+
const { limit } = args;
|
|
1724
|
+
try {
|
|
1725
|
+
const res = await routeGet("/api/polymarket/leaderboard", limit ? { limit } : void 0);
|
|
1726
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1727
|
+
} catch (error) {
|
|
1728
|
+
return err(`Polymarket leaderboard fetch failed: ${extractApiError(error)}`);
|
|
1729
|
+
}
|
|
1730
|
+
};
|
|
1731
|
+
var getPolymarketTrader = async (args) => {
|
|
1732
|
+
const { address } = args;
|
|
1733
|
+
try {
|
|
1734
|
+
const res = await routeGet(`/api/polymarket/trader/${encodeURIComponent(address)}`);
|
|
1735
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1736
|
+
} catch (error) {
|
|
1737
|
+
return err(`Polymarket trader fetch failed: ${extractApiError(error)}`);
|
|
1738
|
+
}
|
|
1739
|
+
};
|
|
1740
|
+
var getPolymarketOrderBook = async (args) => {
|
|
1741
|
+
const { tokenId } = args;
|
|
1742
|
+
try {
|
|
1743
|
+
const res = await routeGet(`/api/polymarket/orderbook/${encodeURIComponent(tokenId)}`);
|
|
1744
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1745
|
+
} catch (error) {
|
|
1746
|
+
return err(`Polymarket order book fetch failed: ${extractApiError(error)}`);
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
var getPolymarketTrades = async (args) => {
|
|
1750
|
+
const { conditionId, limit } = args;
|
|
1751
|
+
try {
|
|
1752
|
+
const res = await routeGet(`/api/polymarket/trades/${encodeURIComponent(conditionId)}`, limit ? { limit } : void 0);
|
|
1753
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1754
|
+
} catch (error) {
|
|
1755
|
+
return err(`Polymarket trades fetch failed: ${extractApiError(error)}`);
|
|
1756
|
+
}
|
|
1757
|
+
};
|
|
1758
|
+
var getPolymarketHistory = async (args) => {
|
|
1759
|
+
const { conditionId, interval, limit } = args;
|
|
1760
|
+
const params = {};
|
|
1761
|
+
if (interval) params.interval = interval;
|
|
1762
|
+
if (limit !== void 0) params.limit = limit;
|
|
1763
|
+
try {
|
|
1764
|
+
const res = await routeGet(`/api/polymarket/history/${encodeURIComponent(conditionId)}`, params);
|
|
1765
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1766
|
+
} catch (error) {
|
|
1767
|
+
return err(`Polymarket history fetch failed: ${extractApiError(error)}`);
|
|
1768
|
+
}
|
|
1769
|
+
};
|
|
1770
|
+
var getPortfolioTokens = async (args) => {
|
|
1771
|
+
const { address, chainId } = args;
|
|
1772
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(address);
|
|
1773
|
+
if (resolveErr) return resolveErr;
|
|
1774
|
+
try {
|
|
1775
|
+
const res = await routeGet(`/api/portfolio/${encodeURIComponent(resolvedAddr || address)}/tokens`, {
|
|
1776
|
+
chain: chainId
|
|
1777
|
+
});
|
|
1778
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1779
|
+
} catch (error) {
|
|
1780
|
+
return err(`Portfolio tokens fetch failed: ${extractApiError(error)}`);
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1783
|
+
var getPortfolioNfts = async (args) => {
|
|
1784
|
+
const { address, chainId } = args;
|
|
1785
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(address);
|
|
1786
|
+
if (resolveErr) return resolveErr;
|
|
1787
|
+
try {
|
|
1788
|
+
const res = await routeGet(`/api/portfolio/${encodeURIComponent(resolvedAddr || address)}/nfts`, {
|
|
1789
|
+
chain: chainId
|
|
1790
|
+
});
|
|
1791
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1792
|
+
} catch (error) {
|
|
1793
|
+
return err(`Portfolio NFTs fetch failed: ${extractApiError(error)}`);
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
var getPortfolioActivity = async (args) => {
|
|
1797
|
+
const { address, chainId } = args;
|
|
1798
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(address);
|
|
1799
|
+
if (resolveErr) return resolveErr;
|
|
1800
|
+
try {
|
|
1801
|
+
const res = await routeGet(`/api/portfolio/${encodeURIComponent(resolvedAddr || address)}/activity`, {
|
|
1802
|
+
chain: chainId
|
|
1803
|
+
});
|
|
1804
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1805
|
+
} catch (error) {
|
|
1806
|
+
return err(`Portfolio activity fetch failed: ${extractApiError(error)}`);
|
|
1807
|
+
}
|
|
1808
|
+
};
|
|
1809
|
+
var getPortfolioStablecoins = async (args) => {
|
|
1810
|
+
const { address, chainId } = args;
|
|
1811
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(address);
|
|
1812
|
+
if (resolveErr) return resolveErr;
|
|
1813
|
+
try {
|
|
1814
|
+
const res = await routeGet(`/api/portfolio/${encodeURIComponent(resolvedAddr || address)}/stablecoins`, {
|
|
1815
|
+
chain: chainId
|
|
1816
|
+
});
|
|
1817
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1818
|
+
} catch (error) {
|
|
1819
|
+
return err(`Portfolio stablecoins fetch failed: ${extractApiError(error)}`);
|
|
1820
|
+
}
|
|
1821
|
+
};
|
|
1822
|
+
var batchLookupContracts = async (args) => {
|
|
1823
|
+
const { addresses } = args;
|
|
1824
|
+
const list = addresses.split(",").map((a) => a.trim()).filter(Boolean);
|
|
1825
|
+
const resolved = [];
|
|
1826
|
+
for (const a of list) {
|
|
1827
|
+
const r = await resolveAddressInput(a);
|
|
1828
|
+
if (r.error) return r.error;
|
|
1829
|
+
resolved.push(r.resolved || a);
|
|
1830
|
+
}
|
|
1831
|
+
try {
|
|
1832
|
+
const res = await routePost("/api/contracts/batch", { addresses: resolved });
|
|
1833
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1834
|
+
} catch (error) {
|
|
1835
|
+
return err(`Batch contract lookup failed: ${extractApiError(error)}`);
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
var refreshContracts = async () => {
|
|
1839
|
+
try {
|
|
1840
|
+
const res = await routePost("/api/contracts/refresh", {});
|
|
1841
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1842
|
+
} catch (error) {
|
|
1843
|
+
return err(`Contract refresh failed: ${extractApiError(error)}`);
|
|
1844
|
+
}
|
|
1845
|
+
};
|
|
1846
|
+
var fetchDuneInteractors = async (args) => {
|
|
1847
|
+
const { contractAddress, chain, limit, customApiKey } = args;
|
|
1848
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(contractAddress);
|
|
1849
|
+
if (resolveErr) return resolveErr;
|
|
1850
|
+
try {
|
|
1851
|
+
const res = await routePost("/api/dune/fetch", {
|
|
1852
|
+
contractAddress: resolvedAddr || contractAddress,
|
|
1853
|
+
chain,
|
|
1854
|
+
...limit !== void 0 ? { limit } : {},
|
|
1855
|
+
...customApiKey ? { customApiKey } : {}
|
|
1856
|
+
});
|
|
1857
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1858
|
+
} catch (error) {
|
|
1859
|
+
return err(`Dune interactor fetch failed: ${extractApiError(error)}`);
|
|
1860
|
+
}
|
|
1861
|
+
};
|
|
275
1862
|
function withLogging(toolName, handler) {
|
|
276
1863
|
return async (args, ctx) => {
|
|
277
1864
|
_mcpCtx = ctx;
|
|
@@ -306,185 +1893,80 @@ function withLogging(toolName, handler) {
|
|
|
306
1893
|
}
|
|
307
1894
|
};
|
|
308
1895
|
}
|
|
309
|
-
var
|
|
310
|
-
|
|
311
|
-
"
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
});
|
|
382
|
-
return ok(JSON.stringify(res.data, null, 2));
|
|
383
|
-
} catch (error) {
|
|
384
|
-
const msg = error.response?.data?.error || error.message;
|
|
385
|
-
return err(`Sybil detection failed: ${msg}`);
|
|
386
|
-
}
|
|
387
|
-
};
|
|
388
|
-
getPortfolio = async (args, ctx) => {
|
|
389
|
-
const { address, chainId } = args;
|
|
390
|
-
try {
|
|
391
|
-
const res = await api().get(`/api/portfolio/${address}`, {
|
|
392
|
-
params: { chain: chainId }
|
|
393
|
-
});
|
|
394
|
-
return ok(JSON.stringify(res.data, null, 2));
|
|
395
|
-
} catch (error) {
|
|
396
|
-
const msg = error.response?.data?.error || error.message;
|
|
397
|
-
return err(`Portfolio fetch failed: ${msg}`);
|
|
398
|
-
}
|
|
399
|
-
};
|
|
400
|
-
getTransactions = async (args, ctx) => {
|
|
401
|
-
const { address, chainId, limit = 50 } = args;
|
|
402
|
-
try {
|
|
403
|
-
const res = await api().post("/api/history", {
|
|
404
|
-
wallet: address,
|
|
405
|
-
blockchain: chainId,
|
|
406
|
-
pageToken: null,
|
|
407
|
-
filters: {}
|
|
408
|
-
});
|
|
409
|
-
const txs = (res.data.transactions || []).slice(0, limit);
|
|
410
|
-
return ok(JSON.stringify({
|
|
411
|
-
address,
|
|
412
|
-
chainId,
|
|
413
|
-
transactions: txs,
|
|
414
|
-
totalCount: res.data.transactions?.length || 0
|
|
415
|
-
}, null, 2));
|
|
416
|
-
} catch (error) {
|
|
417
|
-
const msg = error.response?.data?.error || error.message;
|
|
418
|
-
return err(`Transaction fetch failed: ${msg}`);
|
|
419
|
-
}
|
|
420
|
-
};
|
|
421
|
-
lookupEntity = async (args, ctx) => {
|
|
422
|
-
const { query, chainId } = args;
|
|
423
|
-
const chain = chainId || "ethereum";
|
|
424
|
-
try {
|
|
425
|
-
if (/^0x[a-fA-F0-9]{40}$/.test(query) || /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(query)) {
|
|
426
|
-
const res2 = await api().get(`/api/entities/${query}`, {
|
|
427
|
-
params: { chain }
|
|
428
|
-
});
|
|
429
|
-
return ok(JSON.stringify(res2.data, null, 2));
|
|
430
|
-
}
|
|
431
|
-
const res = await api().get("/api/entities/search", {
|
|
432
|
-
params: { q: query, chain }
|
|
433
|
-
});
|
|
434
|
-
return ok(JSON.stringify(res.data, null, 2));
|
|
435
|
-
} catch (error) {
|
|
436
|
-
if (error.response?.status === 404) {
|
|
437
|
-
return ok(JSON.stringify({ query, label: "Unknown address", chain }, null, 2));
|
|
438
|
-
}
|
|
439
|
-
const msg = error.response?.data?.error || error.message;
|
|
440
|
-
return err(`Entity lookup failed: ${msg}`);
|
|
441
|
-
}
|
|
442
|
-
};
|
|
443
|
-
getGasPrices = async (args, ctx) => {
|
|
444
|
-
const { chainId } = args;
|
|
445
|
-
try {
|
|
446
|
-
const res = await api().get("/api/gas", {
|
|
447
|
-
params: chainId ? { chain: chainId } : {}
|
|
448
|
-
});
|
|
449
|
-
return ok(JSON.stringify(res.data, null, 2));
|
|
450
|
-
} catch (error) {
|
|
451
|
-
const msg = error.response?.data?.error || error.message;
|
|
452
|
-
return err(`Gas price fetch failed: ${msg}`);
|
|
453
|
-
}
|
|
454
|
-
};
|
|
455
|
-
getTokenInfo = async (args, ctx) => {
|
|
456
|
-
const { tokenAddress, chainId } = args;
|
|
457
|
-
try {
|
|
458
|
-
const res = await api().get("/api/market/coins", {
|
|
459
|
-
params: { address: tokenAddress, chainId }
|
|
460
|
-
});
|
|
461
|
-
return ok(JSON.stringify(res.data, null, 2));
|
|
462
|
-
} catch (error) {
|
|
463
|
-
const msg = error.response?.data?.error || error.message;
|
|
464
|
-
return err(`Token info fetch failed: ${msg}`);
|
|
465
|
-
}
|
|
466
|
-
};
|
|
467
|
-
TOOL_HANDLERS = {
|
|
468
|
-
analyze_wallet: withLogging("analyze_wallet", analyzeWallet),
|
|
469
|
-
trace_funds: withLogging("trace_funds", traceFunds),
|
|
470
|
-
compare_wallets: withLogging("compare_wallets", compareWallets),
|
|
471
|
-
analyze_contract: withLogging("analyze_contract", analyzeContract),
|
|
472
|
-
detect_sybil_clusters: withLogging("detect_sybil_clusters", detectSybilClusters),
|
|
473
|
-
get_portfolio: withLogging("get_portfolio", getPortfolio),
|
|
474
|
-
get_transactions: withLogging("get_transactions", getTransactions),
|
|
475
|
-
lookup_entity: withLogging("lookup_entity", lookupEntity),
|
|
476
|
-
get_gas_prices: withLogging("get_gas_prices", getGasPrices),
|
|
477
|
-
get_token_info: withLogging("get_token_info", getTokenInfo)
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
});
|
|
1896
|
+
var TOOL_HANDLERS = {
|
|
1897
|
+
analyze_wallet: withLogging("analyze_wallet", analyzeWallet),
|
|
1898
|
+
trace_funds: withLogging("trace_funds", traceFunds),
|
|
1899
|
+
compare_wallets: withLogging("compare_wallets", compareWallets),
|
|
1900
|
+
analyze_contract: withLogging("analyze_contract", analyzeContract),
|
|
1901
|
+
detect_sybil_clusters: withLogging("detect_sybil_clusters", detectSybilClusters),
|
|
1902
|
+
analyze_cex_flow: withLogging("analyze_cex_flow", analyzeCEXFlow),
|
|
1903
|
+
analyze_sybil_addresses: withLogging("analyze_sybil_addresses", analyzeSybilAddresses),
|
|
1904
|
+
resolve_name: withLogging("resolve_name", resolveName),
|
|
1905
|
+
get_portfolio: withLogging("get_portfolio", getPortfolio),
|
|
1906
|
+
get_portfolio_tokens: withLogging("get_portfolio_tokens", getPortfolioTokens),
|
|
1907
|
+
get_portfolio_nfts: withLogging("get_portfolio_nfts", getPortfolioNfts),
|
|
1908
|
+
get_portfolio_activity: withLogging("get_portfolio_activity", getPortfolioActivity),
|
|
1909
|
+
get_portfolio_stablecoins: withLogging("get_portfolio_stablecoins", getPortfolioStablecoins),
|
|
1910
|
+
get_transactions: withLogging("get_transactions", getTransactions),
|
|
1911
|
+
get_transaction_history: withLogging("get_transaction_history", getTransactionHistory),
|
|
1912
|
+
get_transaction_detail: withLogging("get_transaction_detail", getTransactionDetail),
|
|
1913
|
+
lookup_entity: withLogging("lookup_entity", lookupEntity),
|
|
1914
|
+
search_contracts: withLogging("search_contracts", searchContracts),
|
|
1915
|
+
lookup_contract: withLogging("lookup_contract", lookupContract),
|
|
1916
|
+
batch_lookup_contracts: withLogging("batch_lookup_contracts", batchLookupContracts),
|
|
1917
|
+
get_contract_stats: withLogging("get_contract_stats", getContractStats),
|
|
1918
|
+
refresh_contracts: withLogging("refresh_contracts", refreshContracts),
|
|
1919
|
+
search_tokens: withLogging("search_tokens", searchTokens),
|
|
1920
|
+
get_market_stats: withLogging("get_market_stats", getMarketStats),
|
|
1921
|
+
get_market_coins: withLogging("get_market_coins", getMarketCoins),
|
|
1922
|
+
get_gas_prices: withLogging("get_gas_prices", getGasPrices),
|
|
1923
|
+
get_token_info: withLogging("get_token_info", getTokenInfo),
|
|
1924
|
+
get_token_chart: withLogging("get_token_chart", getTokenChart),
|
|
1925
|
+
get_dexscreener_latest_profiles: withLogging("get_dexscreener_latest_profiles", getDexScreenerLatestProfiles),
|
|
1926
|
+
get_dexscreener_top_boosts: withLogging("get_dexscreener_top_boosts", getDexScreenerBoosts),
|
|
1927
|
+
search_dexscreener_pairs: withLogging("search_dexscreener_pairs", searchDexScreenerPairs),
|
|
1928
|
+
get_dexscreener_token_details: withLogging("get_dexscreener_token_details", getDexScreenerTokenDetails),
|
|
1929
|
+
get_dexscreener_token_pairs: withLogging("get_dexscreener_token_pairs", getDexScreenerTokenPairs),
|
|
1930
|
+
get_dexscreener_trending: withLogging("get_dexscreener_trending", getDexScreenerTrending),
|
|
1931
|
+
get_scan_history: withLogging("get_scan_history", getScanHistory),
|
|
1932
|
+
get_scan_history_stats: withLogging("get_scan_history_stats", getScanHistoryStats),
|
|
1933
|
+
save_scan_history_item: withLogging("save_scan_history_item", saveScanHistoryItem),
|
|
1934
|
+
sync_scan_history: withLogging("sync_scan_history", syncScanHistory),
|
|
1935
|
+
delete_scan_history_item: withLogging("delete_scan_history_item", deleteScanHistoryItem),
|
|
1936
|
+
clear_scan_history: withLogging("clear_scan_history", clearScanHistory),
|
|
1937
|
+
list_rooms: withLogging("list_rooms", listRooms),
|
|
1938
|
+
get_room_details: withLogging("get_room_details", getRoomDetails),
|
|
1939
|
+
create_room: withLogging("create_room", createRoom),
|
|
1940
|
+
update_room: withLogging("update_room", updateRoom),
|
|
1941
|
+
delete_room: withLogging("delete_room", deleteRoom),
|
|
1942
|
+
list_room_messages: withLogging("list_room_messages", listRoomMessages),
|
|
1943
|
+
send_room_message: withLogging("send_room_message", sendRoomMessage),
|
|
1944
|
+
join_room: withLogging("join_room", joinRoom),
|
|
1945
|
+
leave_room: withLogging("leave_room", leaveRoom),
|
|
1946
|
+
remove_room_member: withLogging("remove_room_member", removeRoomMember),
|
|
1947
|
+
promote_room_member: withLogging("promote_room_member", promoteRoomMember),
|
|
1948
|
+
create_room_invite: withLogging("create_room_invite", createRoomInvite),
|
|
1949
|
+
list_room_pins: withLogging("list_room_pins", listRoomPins),
|
|
1950
|
+
pin_room_message: withLogging("pin_room_message", pinRoomMessage),
|
|
1951
|
+
unpin_room_message: withLogging("unpin_room_message", unpinRoomMessage),
|
|
1952
|
+
edit_room_message: withLogging("edit_room_message", editRoomMessage),
|
|
1953
|
+
delete_room_message: withLogging("delete_room_message", deleteRoomMessage),
|
|
1954
|
+
export_room: withLogging("export_room", exportRoom),
|
|
1955
|
+
get_polymarket_markets: withLogging("get_polymarket_markets", getPolymarketMarkets),
|
|
1956
|
+
get_polymarket_market: withLogging("get_polymarket_market", getPolymarketMarket),
|
|
1957
|
+
get_polymarket_trending: withLogging("get_polymarket_trending", getPolymarketTrending),
|
|
1958
|
+
get_polymarket_spikes: withLogging("get_polymarket_spikes", getPolymarketSpikes),
|
|
1959
|
+
get_polymarket_movers: withLogging("get_polymarket_movers", getPolymarketMovers),
|
|
1960
|
+
get_polymarket_events: withLogging("get_polymarket_events", getPolymarketEvents),
|
|
1961
|
+
get_polymarket_leaderboard: withLogging("get_polymarket_leaderboard", getPolymarketLeaderboard),
|
|
1962
|
+
get_polymarket_trader: withLogging("get_polymarket_trader", getPolymarketTrader),
|
|
1963
|
+
get_polymarket_order_book: withLogging("get_polymarket_order_book", getPolymarketOrderBook),
|
|
1964
|
+
get_polymarket_trades: withLogging("get_polymarket_trades", getPolymarketTrades),
|
|
1965
|
+
get_polymarket_history: withLogging("get_polymarket_history", getPolymarketHistory),
|
|
1966
|
+
fetch_dune_interactors: withLogging("fetch_dune_interactors", fetchDuneInteractors)
|
|
1967
|
+
};
|
|
481
1968
|
|
|
482
1969
|
// src/mcp/mcpAuth.ts
|
|
483
|
-
var mcpAuth_exports = {};
|
|
484
|
-
__export(mcpAuth_exports, {
|
|
485
|
-
mcpApiKeyAuth: () => mcpApiKeyAuth,
|
|
486
|
-
validateMcpApiKey: () => validateMcpApiKey
|
|
487
|
-
});
|
|
488
1970
|
async function validateMcpApiKey(rawKey) {
|
|
489
1971
|
if (!rawKey.startsWith("ft_")) throw new Error("Invalid MCP API key format");
|
|
490
1972
|
let firestoreResult = null;
|
|
@@ -542,8 +2024,8 @@ async function validateWithFirestore(rawKey) {
|
|
|
542
2024
|
}
|
|
543
2025
|
async function validateViaHttp(rawKey) {
|
|
544
2026
|
const API_URL = process.env.FUNDTRACER_API_URL || "https://api.fundtracer.xyz";
|
|
545
|
-
const { default:
|
|
546
|
-
const res = await
|
|
2027
|
+
const { default: fetch2 } = await import("node-fetch");
|
|
2028
|
+
const res = await fetch2(`${API_URL}/api/mcp/validate`, {
|
|
547
2029
|
method: "POST",
|
|
548
2030
|
headers: {
|
|
549
2031
|
"Content-Type": "application/json",
|
|
@@ -569,69 +2051,27 @@ async function trackUsage(userId, rawKey) {
|
|
|
569
2051
|
} catch {
|
|
570
2052
|
}
|
|
571
2053
|
}
|
|
572
|
-
async function mcpApiKeyAuth(req, res, next) {
|
|
573
|
-
const authHeader = req.headers.authorization;
|
|
574
|
-
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
575
|
-
return res.status(401).json({ error: "MCP API key required (Authorization: Bearer ft_mcp_<key>)" });
|
|
576
|
-
}
|
|
577
|
-
const rawKey = authHeader.slice(7).trim();
|
|
578
|
-
if (!rawKey.startsWith("ft_")) {
|
|
579
|
-
return res.status(401).json({ error: "Invalid MCP API key format" });
|
|
580
|
-
}
|
|
581
|
-
try {
|
|
582
|
-
const ctx = await validateMcpApiKey(rawKey);
|
|
583
|
-
req.mcpContext = ctx;
|
|
584
|
-
next();
|
|
585
|
-
} catch (err2) {
|
|
586
|
-
return res.status(401).json({ error: err2.message });
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
var init_mcpAuth = __esm({
|
|
590
|
-
"src/mcp/mcpAuth.ts"() {
|
|
591
|
-
}
|
|
592
|
-
});
|
|
593
2054
|
|
|
594
|
-
// src/mcp/
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
dotenv.config();
|
|
598
|
-
async function main() {
|
|
599
|
-
let firebaseAvailable = false;
|
|
600
|
-
try {
|
|
601
|
-
const { initializeFirebase } = await import("../firebase.js");
|
|
602
|
-
initializeFirebase();
|
|
603
|
-
firebaseAvailable = true;
|
|
604
|
-
console.error("[MCP] Firebase initialized");
|
|
605
|
-
} catch (err2) {
|
|
606
|
-
console.error("[MCP] Firebase not available \u2014 key validation will fail. Set Firebase credentials in env.");
|
|
607
|
-
}
|
|
608
|
-
const { ALL_MCP_TOOLS: ALL_MCP_TOOLS2 } = await Promise.resolve().then(() => (init_tools(), tools_exports));
|
|
609
|
-
const { TOOL_HANDLERS: TOOL_HANDLERS2 } = await Promise.resolve().then(() => (init_api_handlers(), api_handlers_exports));
|
|
610
|
-
const { validateMcpApiKey: validateMcpApiKey2 } = await Promise.resolve().then(() => (init_mcpAuth(), mcpAuth_exports));
|
|
2055
|
+
// src/mcp/server.ts
|
|
2056
|
+
function createFundTracerMcpServer(resolveContext, options = {}) {
|
|
2057
|
+
const logRegistrations = options.logRegistrations ?? true;
|
|
611
2058
|
const server = new McpServer({
|
|
612
2059
|
name: "FundTracer MCP",
|
|
613
|
-
version: "1.
|
|
2060
|
+
version: "1.1.0"
|
|
614
2061
|
});
|
|
615
|
-
for (const toolDef of
|
|
616
|
-
const handler =
|
|
2062
|
+
for (const toolDef of ALL_MCP_TOOLS) {
|
|
2063
|
+
const handler = TOOL_HANDLERS[toolDef.name];
|
|
617
2064
|
if (!handler) {
|
|
618
2065
|
console.error(`[MCP] No handler for tool: ${toolDef.name}`);
|
|
619
2066
|
continue;
|
|
620
2067
|
}
|
|
621
2068
|
server.registerTool(toolDef.name, {
|
|
622
2069
|
description: toolDef.description,
|
|
623
|
-
inputSchema:
|
|
624
|
-
}, async (args) => {
|
|
625
|
-
const apiKey = process.env.FUNDTRACER_MCP_API_KEY;
|
|
626
|
-
if (!apiKey) {
|
|
627
|
-
return {
|
|
628
|
-
content: [{ type: "text", text: "FUNDTRACER_MCP_API_KEY environment variable not set" }],
|
|
629
|
-
isError: true
|
|
630
|
-
};
|
|
631
|
-
}
|
|
2070
|
+
inputSchema: jsonSchemaObjectToZodShape(toolDef.inputSchema)
|
|
2071
|
+
}, async (args, requestContext) => {
|
|
632
2072
|
let ctx;
|
|
633
2073
|
try {
|
|
634
|
-
ctx = await
|
|
2074
|
+
ctx = await resolveContext(requestContext);
|
|
635
2075
|
} catch (err2) {
|
|
636
2076
|
return {
|
|
637
2077
|
content: [{ type: "text", text: `Authentication failed: ${err2.message}` }],
|
|
@@ -640,10 +2080,83 @@ async function main() {
|
|
|
640
2080
|
}
|
|
641
2081
|
return handler(args, ctx);
|
|
642
2082
|
});
|
|
643
|
-
|
|
2083
|
+
if (logRegistrations) {
|
|
2084
|
+
console.error(`[MCP] Registered tool: ${toolDef.name}`);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
return server;
|
|
2088
|
+
}
|
|
2089
|
+
function jsonSchemaObjectToZodShape(schema) {
|
|
2090
|
+
const required = new Set(Array.isArray(schema?.required) ? schema.required : []);
|
|
2091
|
+
const properties = schema?.properties || {};
|
|
2092
|
+
const shape = {};
|
|
2093
|
+
for (const [name, propertySchema] of Object.entries(properties)) {
|
|
2094
|
+
let field = jsonSchemaPropertyToZod(propertySchema);
|
|
2095
|
+
if (!required.has(name)) field = field.optional();
|
|
2096
|
+
shape[name] = field;
|
|
2097
|
+
}
|
|
2098
|
+
return shape;
|
|
2099
|
+
}
|
|
2100
|
+
function jsonSchemaPropertyToZod(schema) {
|
|
2101
|
+
let field;
|
|
2102
|
+
if (Array.isArray(schema?.enum) && schema.enum.length > 0) {
|
|
2103
|
+
const values = schema.enum.filter((value) => typeof value === "string");
|
|
2104
|
+
field = values.length > 0 ? z.enum(values) : z.string();
|
|
2105
|
+
} else {
|
|
2106
|
+
switch (schema?.type) {
|
|
2107
|
+
case "number":
|
|
2108
|
+
case "integer":
|
|
2109
|
+
field = z.number();
|
|
2110
|
+
break;
|
|
2111
|
+
case "boolean":
|
|
2112
|
+
field = z.boolean();
|
|
2113
|
+
break;
|
|
2114
|
+
case "array":
|
|
2115
|
+
field = z.array(z.unknown());
|
|
2116
|
+
break;
|
|
2117
|
+
case "object":
|
|
2118
|
+
field = z.record(z.unknown());
|
|
2119
|
+
break;
|
|
2120
|
+
case "string":
|
|
2121
|
+
default:
|
|
2122
|
+
field = z.string();
|
|
2123
|
+
break;
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
if (schema?.description && typeof field.describe === "function") {
|
|
2127
|
+
field = field.describe(schema.description);
|
|
2128
|
+
}
|
|
2129
|
+
if (schema?.default !== void 0) {
|
|
2130
|
+
field = field.default(schema.default);
|
|
2131
|
+
}
|
|
2132
|
+
return field;
|
|
2133
|
+
}
|
|
2134
|
+
async function resolveStdioMcpContext() {
|
|
2135
|
+
const apiKey = process.env.FUNDTRACER_MCP_API_KEY;
|
|
2136
|
+
if (!apiKey) {
|
|
2137
|
+
throw new Error("FUNDTRACER_MCP_API_KEY environment variable not set");
|
|
2138
|
+
}
|
|
2139
|
+
return validateMcpApiKey(apiKey);
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
// src/mcp/stdio.ts
|
|
2143
|
+
dotenv.config();
|
|
2144
|
+
console.log = console.error.bind(console);
|
|
2145
|
+
process.env.FUNDTRACER_MCP_DISABLE_LOGGING = process.env.FUNDTRACER_MCP_DISABLE_LOGGING || "1";
|
|
2146
|
+
async function main() {
|
|
2147
|
+
let firebaseAvailable = false;
|
|
2148
|
+
try {
|
|
2149
|
+
const { initializeFirebase } = await import("../firebase.js");
|
|
2150
|
+
initializeFirebase();
|
|
2151
|
+
firebaseAvailable = true;
|
|
2152
|
+
console.error("[MCP] Firebase initialized");
|
|
2153
|
+
} catch (err2) {
|
|
2154
|
+
console.error("[MCP] Firebase not available \u2014 key validation will fail. Set Firebase credentials in env.");
|
|
644
2155
|
}
|
|
2156
|
+
const server = createFundTracerMcpServer(resolveStdioMcpContext);
|
|
645
2157
|
const transport = new StdioServerTransport();
|
|
646
2158
|
await server.connect(transport);
|
|
2159
|
+
process.stdin.resume();
|
|
647
2160
|
console.error("[MCP] FundTracer MCP server running on stdio");
|
|
648
2161
|
}
|
|
649
2162
|
main().catch((err2) => {
|
|
@@ -652,7 +2165,6 @@ main().catch((err2) => {
|
|
|
652
2165
|
});
|
|
653
2166
|
process.on("SIGINT", async () => {
|
|
654
2167
|
console.error("[MCP] Shutting down...");
|
|
655
|
-
const { McpServer: McpServer2 } = await import("@modelcontextprotocol/server");
|
|
656
2168
|
process.exit(0);
|
|
657
2169
|
});
|
|
658
2170
|
process.on("SIGTERM", async () => {
|