@fundtracer/mcp 1.0.11 → 1.0.12

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.
Files changed (2) hide show
  1. package/fundtracer-mcp.js +438 -437
  2. package/package.json +4 -3
package/fundtracer-mcp.js CHANGED
@@ -1,226 +1,219 @@
1
1
  #!/usr/bin/env node
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropNames = Object.getOwnPropertyNames;
4
- var __esm = (fn, res) => function __init() {
5
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
- };
7
- var __export = (target, all) => {
8
- for (var name in all)
9
- __defProp(target, name, { get: all[name], enumerable: true });
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 tools_exports = {};
14
- __export(tools_exports, {
15
- ALL_MCP_TOOLS: () => ALL_MCP_TOOLS,
16
- getToolByName: () => getToolByName
17
- });
18
- function getToolByName(name) {
19
- return ALL_MCP_TOOLS.find((t) => t.name === name);
20
- }
21
- var ALL_MCP_TOOLS;
22
- var init_tools = __esm({
23
- "src/mcp/tools.ts"() {
24
- ALL_MCP_TOOLS = [
25
- {
26
- name: "analyze_wallet",
27
- description: "Perform a full blockchain wallet analysis including balance, transactions, risk score, suspicious indicators, and project interactions.",
28
- inputSchema: {
29
- type: "object",
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
- name: "trace_funds",
48
- description: "Trace funding sources and destinations for a wallet address, building a recursive funding tree.",
49
- inputSchema: {
50
- type: "object",
51
- properties: {
52
- address: { type: "string", description: "Wallet address to trace" },
53
- chainId: {
54
- type: "string",
55
- description: "Blockchain to trace on",
56
- enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", "solana"]
57
- },
58
- maxDepth: {
59
- type: "number",
60
- description: "How many levels deep to trace (default: 3)",
61
- default: 3
62
- },
63
- direction: {
64
- type: "string",
65
- description: "Which direction to trace",
66
- enum: ["sources", "destinations", "both"],
67
- default: "both"
68
- }
69
- },
70
- required: ["address", "chainId"]
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
- name: "compare_wallets",
75
- description: "Compare multiple wallet addresses for common funding sources, shared project interactions, and sybil correlation scoring.",
76
- inputSchema: {
77
- type: "object",
78
- properties: {
79
- addresses: {
80
- type: "string",
81
- description: "Comma-separated list of wallet addresses to compare (2-20 wallets)"
82
- },
83
- chainId: {
84
- type: "string",
85
- description: "Blockchain to compare on",
86
- enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", "solana"]
87
- }
88
- },
89
- required: ["addresses", "chainId"]
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
- name: "analyze_contract",
94
- description: "Analyze all addresses that have interacted with a smart contract, detecting sybil clusters and shared funding sources.",
95
- inputSchema: {
96
- type: "object",
97
- properties: {
98
- contractAddress: { type: "string", description: "Smart contract address to analyze" },
99
- chainId: {
100
- type: "string",
101
- description: "Blockchain the contract is on",
102
- enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc"]
103
- },
104
- maxInteractors: {
105
- type: "number",
106
- description: "Max interactors to analyze (default: 100)",
107
- default: 100
108
- }
109
- },
110
- required: ["contractAddress", "chainId"]
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
- name: "detect_sybil_clusters",
115
- description: "Detect sybil (fake) accounts by clustering wallets that share common funding sources.",
116
- inputSchema: {
117
- type: "object",
118
- properties: {
119
- addresses: {
120
- type: "string",
121
- description: "Comma-separated list of wallet addresses to check for sybil clustering"
122
- },
123
- chainId: {
124
- type: "string",
125
- description: "Blockchain to analyze on",
126
- enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "bsc"]
127
- }
128
- },
129
- required: ["addresses", "chainId"]
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
- name: "get_portfolio",
134
- description: "Get the token portfolio, DeFi positions, and NFT holdings for a wallet address.",
135
- inputSchema: {
136
- type: "object",
137
- properties: {
138
- address: { type: "string", description: "Wallet address" },
139
- chainId: {
140
- type: "string",
141
- description: "Blockchain",
142
- enum: ["ethereum", "solana", "base", "arbitrum", "optimism", "polygon", "linea"]
143
- }
144
- },
145
- required: ["address", "chainId"]
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
- name: "get_transactions",
150
- description: "Get recent transaction history for a wallet address.",
151
- inputSchema: {
152
- type: "object",
153
- properties: {
154
- address: { type: "string", description: "Wallet address" },
155
- chainId: {
156
- type: "string",
157
- description: "Blockchain",
158
- enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", "solana"]
159
- },
160
- limit: {
161
- type: "number",
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_transactions",
138
+ description: "Get recent transaction history 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", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", "solana"]
147
+ },
148
+ limit: {
149
+ type: "number",
150
+ description: "Number of transactions to return (default: 50)",
151
+ default: 50
167
152
  }
168
153
  },
169
- {
170
- name: "lookup_entity",
171
- description: "Look up a known blockchain entity, protocol, or address label.",
172
- inputSchema: {
173
- type: "object",
174
- properties: {
175
- query: { type: "string", description: "Entity name, address, or label to look up" },
176
- chainId: {
177
- type: "string",
178
- description: "Blockchain to search (optional)",
179
- enum: ["ethereum", "solana", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", ""],
180
- default: ""
181
- }
182
- },
183
- required: ["query"]
154
+ required: ["address", "chainId"]
155
+ }
156
+ },
157
+ {
158
+ name: "lookup_entity",
159
+ description: "Look up a known blockchain entity, protocol, or address label.",
160
+ inputSchema: {
161
+ type: "object",
162
+ properties: {
163
+ query: { type: "string", description: "Entity name, address, or label to look up" },
164
+ chainId: {
165
+ type: "string",
166
+ description: "Blockchain to search (optional)",
167
+ enum: ["ethereum", "solana", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", ""],
168
+ default: ""
184
169
  }
185
170
  },
186
- {
187
- name: "get_gas_prices",
188
- description: "Get current gas prices across supported blockchain networks.",
189
- inputSchema: {
190
- type: "object",
191
- properties: {
192
- chainId: {
193
- type: "string",
194
- description: "Specific chain (optional, returns all if omitted)",
195
- enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", ""],
196
- default: ""
197
- }
198
- },
199
- required: []
171
+ required: ["query"]
172
+ }
173
+ },
174
+ {
175
+ name: "get_gas_prices",
176
+ description: "Get current gas prices across supported blockchain networks.",
177
+ inputSchema: {
178
+ type: "object",
179
+ properties: {
180
+ chainId: {
181
+ type: "string",
182
+ description: "Specific chain (optional, returns all if omitted)",
183
+ enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", ""],
184
+ default: ""
200
185
  }
201
186
  },
202
- {
203
- name: "get_token_info",
204
- description: "Get market data and information for a token by address or symbol.",
205
- inputSchema: {
206
- type: "object",
207
- properties: {
208
- tokenAddress: { type: "string", description: "Token contract address" },
209
- chainId: {
210
- type: "string",
211
- description: "Blockchain the token is on",
212
- enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", "solana"]
213
- }
214
- },
215
- required: ["tokenAddress", "chainId"]
187
+ required: []
188
+ }
189
+ },
190
+ {
191
+ name: "get_token_info",
192
+ description: "Get market data and information for a token by address or symbol.",
193
+ inputSchema: {
194
+ type: "object",
195
+ properties: {
196
+ tokenAddress: { type: "string", description: "Token contract address" },
197
+ chainId: {
198
+ type: "string",
199
+ description: "Blockchain the token is on",
200
+ enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", "solana"]
216
201
  }
217
- }
218
- ];
202
+ },
203
+ required: ["tokenAddress", "chainId"]
204
+ }
219
205
  }
220
- });
206
+ ];
207
+
208
+ // src/mcp/api-handlers.ts
209
+ import { default as axios } from "axios";
221
210
 
222
211
  // src/mcp/mcpLogger.ts
212
+ var mcpLogWarnings = 0;
223
213
  async function logMcpRequest(entry) {
214
+ if (process.env.FUNDTRACER_MCP_DISABLE_LOGGING === "1") {
215
+ return;
216
+ }
224
217
  try {
225
218
  const { getFirestore } = await import("../firebase.js");
226
219
  const db = getFirestore();
@@ -237,25 +230,16 @@ async function logMcpRequest(entry) {
237
230
  }
238
231
  }
239
232
  }
240
- var mcpLogWarnings;
241
- var init_mcpLogger = __esm({
242
- "src/mcp/mcpLogger.ts"() {
243
- mcpLogWarnings = 0;
244
- }
245
- });
246
233
 
247
234
  // 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
235
  function ok(text) {
254
236
  return { content: [{ type: "text", text }] };
255
237
  }
256
238
  function err(message) {
257
239
  return { content: [{ type: "text", text: message }], isError: true };
258
240
  }
241
+ var API_BASE = process.env.FUNDTRACER_API_URL || "https://api.fundtracer.xyz";
242
+ var _mcpCtx = null;
259
243
  function api() {
260
244
  const key = _mcpCtx?.apiKey || process.env.FUNDTRACER_MCP_API_KEY || "";
261
245
  const headers = {
@@ -272,6 +256,158 @@ function api() {
272
256
  headers
273
257
  });
274
258
  }
259
+ var analyzeWallet = async (args, ctx) => {
260
+ const { address, chainId, transactionLimit } = args;
261
+ try {
262
+ const res = await api().post("/api/analyze/wallet", {
263
+ address,
264
+ chain: chainId,
265
+ options: { limit: transactionLimit || 500 }
266
+ });
267
+ return ok(JSON.stringify(res.data, null, 2));
268
+ } catch (error) {
269
+ const msg = error.response?.data?.error || error.message;
270
+ return err(`Wallet analysis failed: ${msg}`);
271
+ }
272
+ };
273
+ var traceFunds = async (args, ctx) => {
274
+ const { address, chainId, maxDepth = 3, direction = "both" } = args;
275
+ try {
276
+ const res = await api().post("/api/analyze/funding-tree", {
277
+ address,
278
+ chain: chainId,
279
+ maxDepth,
280
+ direction
281
+ });
282
+ return ok(JSON.stringify(res.data, null, 2));
283
+ } catch (error) {
284
+ const msg = error.response?.data?.error || error.message;
285
+ return err(`Fund tracing failed: ${msg}`);
286
+ }
287
+ };
288
+ var compareWallets = async (args, ctx) => {
289
+ const { addresses, chainId } = args;
290
+ const addrList = addresses.split(",").map((a) => a.trim()).filter(Boolean);
291
+ if (addrList.length < 2) return err("At least 2 addresses required");
292
+ try {
293
+ const res = await api().post("/api/analyze/compare", {
294
+ addresses: addrList,
295
+ chain: chainId
296
+ });
297
+ return ok(JSON.stringify(res.data, null, 2));
298
+ } catch (error) {
299
+ const msg = error.response?.data?.error || error.message;
300
+ return err(`Wallet comparison failed: ${msg}`);
301
+ }
302
+ };
303
+ var analyzeContract = async (args, ctx) => {
304
+ const { contractAddress, chainId, maxInteractors = 100 } = args;
305
+ try {
306
+ const res = await api().post("/api/analyze/contract", {
307
+ contractAddress,
308
+ chain: chainId,
309
+ maxInteractors
310
+ });
311
+ return ok(JSON.stringify(res.data, null, 2));
312
+ } catch (error) {
313
+ const msg = error.response?.data?.error || error.message;
314
+ return err(`Contract analysis failed: ${msg}`);
315
+ }
316
+ };
317
+ var detectSybilClusters = async (args, ctx) => {
318
+ const { addresses, chainId } = args;
319
+ const addrList = addresses.split(",").map((a) => a.trim()).filter(Boolean);
320
+ if (addrList.length < 3) return err("At least 3 addresses required for cluster detection");
321
+ try {
322
+ const res = await api().post("/api/analyze/sybil", {
323
+ addresses: addrList,
324
+ chain: chainId
325
+ });
326
+ return ok(JSON.stringify(res.data, null, 2));
327
+ } catch (error) {
328
+ const msg = error.response?.data?.error || error.message;
329
+ return err(`Sybil detection failed: ${msg}`);
330
+ }
331
+ };
332
+ var getPortfolio = async (args, ctx) => {
333
+ const { address, chainId } = args;
334
+ try {
335
+ const res = await api().get(`/api/portfolio/${address}`, {
336
+ params: { chain: chainId }
337
+ });
338
+ return ok(JSON.stringify(res.data, null, 2));
339
+ } catch (error) {
340
+ const msg = error.response?.data?.error || error.message;
341
+ return err(`Portfolio fetch failed: ${msg}`);
342
+ }
343
+ };
344
+ var getTransactions = async (args, ctx) => {
345
+ const { address, chainId, limit = 50 } = args;
346
+ try {
347
+ const res = await api().post("/api/history", {
348
+ wallet: address,
349
+ blockchain: chainId,
350
+ pageToken: null,
351
+ filters: {}
352
+ });
353
+ const txs = (res.data.transactions || []).slice(0, limit);
354
+ return ok(JSON.stringify({
355
+ address,
356
+ chainId,
357
+ transactions: txs,
358
+ totalCount: res.data.transactions?.length || 0
359
+ }, null, 2));
360
+ } catch (error) {
361
+ const msg = error.response?.data?.error || error.message;
362
+ return err(`Transaction fetch failed: ${msg}`);
363
+ }
364
+ };
365
+ var lookupEntity = async (args, ctx) => {
366
+ const { query, chainId } = args;
367
+ const chain = chainId || "ethereum";
368
+ try {
369
+ if (/^0x[a-fA-F0-9]{40}$/.test(query) || /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(query)) {
370
+ const res2 = await api().get(`/api/entities/${query}`, {
371
+ params: { chain }
372
+ });
373
+ return ok(JSON.stringify(res2.data, null, 2));
374
+ }
375
+ const res = await api().get("/api/entities/search", {
376
+ params: { q: query, chain }
377
+ });
378
+ return ok(JSON.stringify(res.data, null, 2));
379
+ } catch (error) {
380
+ if (error.response?.status === 404) {
381
+ return ok(JSON.stringify({ query, label: "Unknown address", chain }, null, 2));
382
+ }
383
+ const msg = error.response?.data?.error || error.message;
384
+ return err(`Entity lookup failed: ${msg}`);
385
+ }
386
+ };
387
+ var getGasPrices = async (args, ctx) => {
388
+ const { chainId } = args;
389
+ try {
390
+ const res = await api().get("/api/gas", {
391
+ params: chainId ? { chain: chainId } : {}
392
+ });
393
+ return ok(JSON.stringify(res.data, null, 2));
394
+ } catch (error) {
395
+ const msg = error.response?.data?.error || error.message;
396
+ return err(`Gas price fetch failed: ${msg}`);
397
+ }
398
+ };
399
+ var getTokenInfo = async (args, ctx) => {
400
+ const { tokenAddress, chainId } = args;
401
+ try {
402
+ const res = await api().get("/api/market/coins", {
403
+ params: { address: tokenAddress, chainId }
404
+ });
405
+ return ok(JSON.stringify(res.data, null, 2));
406
+ } catch (error) {
407
+ const msg = error.response?.data?.error || error.message;
408
+ return err(`Token info fetch failed: ${msg}`);
409
+ }
410
+ };
275
411
  function withLogging(toolName, handler) {
276
412
  return async (args, ctx) => {
277
413
  _mcpCtx = ctx;
@@ -306,185 +442,20 @@ function withLogging(toolName, handler) {
306
442
  }
307
443
  };
308
444
  }
309
- var API_BASE, _mcpCtx, analyzeWallet, traceFunds, compareWallets, analyzeContract, detectSybilClusters, getPortfolio, getTransactions, lookupEntity, getGasPrices, getTokenInfo, TOOL_HANDLERS;
310
- var init_api_handlers = __esm({
311
- "src/mcp/api-handlers.ts"() {
312
- init_mcpLogger();
313
- API_BASE = process.env.FUNDTRACER_API_URL || "https://api.fundtracer.xyz";
314
- _mcpCtx = null;
315
- analyzeWallet = async (args, ctx) => {
316
- const { address, chainId, transactionLimit } = args;
317
- try {
318
- const res = await api().post("/api/analyze/wallet", {
319
- address,
320
- chain: chainId,
321
- options: { limit: transactionLimit || 500 }
322
- });
323
- return ok(JSON.stringify(res.data, null, 2));
324
- } catch (error) {
325
- const msg = error.response?.data?.error || error.message;
326
- return err(`Wallet analysis failed: ${msg}`);
327
- }
328
- };
329
- traceFunds = async (args, ctx) => {
330
- const { address, chainId, maxDepth = 3, direction = "both" } = args;
331
- try {
332
- const res = await api().post("/api/analyze/funding-tree", {
333
- address,
334
- chain: chainId,
335
- maxDepth,
336
- direction
337
- });
338
- return ok(JSON.stringify(res.data, null, 2));
339
- } catch (error) {
340
- const msg = error.response?.data?.error || error.message;
341
- return err(`Fund tracing failed: ${msg}`);
342
- }
343
- };
344
- compareWallets = async (args, ctx) => {
345
- const { addresses, chainId } = args;
346
- const addrList = addresses.split(",").map((a) => a.trim()).filter(Boolean);
347
- if (addrList.length < 2) return err("At least 2 addresses required");
348
- try {
349
- const res = await api().post("/api/analyze/compare", {
350
- addresses: addrList,
351
- chain: chainId
352
- });
353
- return ok(JSON.stringify(res.data, null, 2));
354
- } catch (error) {
355
- const msg = error.response?.data?.error || error.message;
356
- return err(`Wallet comparison failed: ${msg}`);
357
- }
358
- };
359
- analyzeContract = async (args, ctx) => {
360
- const { contractAddress, chainId, maxInteractors = 100 } = args;
361
- try {
362
- const res = await api().post("/api/analyze/contract", {
363
- contractAddress,
364
- chain: chainId,
365
- maxInteractors
366
- });
367
- return ok(JSON.stringify(res.data, null, 2));
368
- } catch (error) {
369
- const msg = error.response?.data?.error || error.message;
370
- return err(`Contract analysis failed: ${msg}`);
371
- }
372
- };
373
- detectSybilClusters = async (args, ctx) => {
374
- const { addresses, chainId } = args;
375
- const addrList = addresses.split(",").map((a) => a.trim()).filter(Boolean);
376
- if (addrList.length < 3) return err("At least 3 addresses required for cluster detection");
377
- try {
378
- const res = await api().post("/api/analyze/sybil", {
379
- addresses: addrList,
380
- chain: chainId
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
- });
445
+ var TOOL_HANDLERS = {
446
+ analyze_wallet: withLogging("analyze_wallet", analyzeWallet),
447
+ trace_funds: withLogging("trace_funds", traceFunds),
448
+ compare_wallets: withLogging("compare_wallets", compareWallets),
449
+ analyze_contract: withLogging("analyze_contract", analyzeContract),
450
+ detect_sybil_clusters: withLogging("detect_sybil_clusters", detectSybilClusters),
451
+ get_portfolio: withLogging("get_portfolio", getPortfolio),
452
+ get_transactions: withLogging("get_transactions", getTransactions),
453
+ lookup_entity: withLogging("lookup_entity", lookupEntity),
454
+ get_gas_prices: withLogging("get_gas_prices", getGasPrices),
455
+ get_token_info: withLogging("get_token_info", getTokenInfo)
456
+ };
481
457
 
482
458
  // src/mcp/mcpAuth.ts
483
- var mcpAuth_exports = {};
484
- __export(mcpAuth_exports, {
485
- mcpApiKeyAuth: () => mcpApiKeyAuth,
486
- validateMcpApiKey: () => validateMcpApiKey
487
- });
488
459
  async function validateMcpApiKey(rawKey) {
489
460
  if (!rawKey.startsWith("ft_")) throw new Error("Invalid MCP API key format");
490
461
  let firestoreResult = null;
@@ -569,69 +540,27 @@ async function trackUsage(userId, rawKey) {
569
540
  } catch {
570
541
  }
571
542
  }
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
543
 
594
- // src/mcp/stdio.ts
595
- import * as dotenv from "dotenv";
596
- import { McpServer, StdioServerTransport, fromJsonSchema } from "@modelcontextprotocol/server";
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));
544
+ // src/mcp/server.ts
545
+ function createFundTracerMcpServer(resolveContext, options = {}) {
546
+ const logRegistrations = options.logRegistrations ?? true;
611
547
  const server = new McpServer({
612
548
  name: "FundTracer MCP",
613
549
  version: "1.0.0"
614
550
  });
615
- for (const toolDef of ALL_MCP_TOOLS2) {
616
- const handler = TOOL_HANDLERS2[toolDef.name];
551
+ for (const toolDef of ALL_MCP_TOOLS) {
552
+ const handler = TOOL_HANDLERS[toolDef.name];
617
553
  if (!handler) {
618
554
  console.error(`[MCP] No handler for tool: ${toolDef.name}`);
619
555
  continue;
620
556
  }
621
557
  server.registerTool(toolDef.name, {
622
558
  description: toolDef.description,
623
- inputSchema: fromJsonSchema(toolDef.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
- }
559
+ inputSchema: jsonSchemaObjectToZodShape(toolDef.inputSchema)
560
+ }, async (args, requestContext) => {
632
561
  let ctx;
633
562
  try {
634
- ctx = await validateMcpApiKey2(apiKey);
563
+ ctx = await resolveContext(requestContext);
635
564
  } catch (err2) {
636
565
  return {
637
566
  content: [{ type: "text", text: `Authentication failed: ${err2.message}` }],
@@ -640,10 +569,83 @@ async function main() {
640
569
  }
641
570
  return handler(args, ctx);
642
571
  });
643
- console.error(`[MCP] Registered tool: ${toolDef.name}`);
572
+ if (logRegistrations) {
573
+ console.error(`[MCP] Registered tool: ${toolDef.name}`);
574
+ }
575
+ }
576
+ return server;
577
+ }
578
+ function jsonSchemaObjectToZodShape(schema) {
579
+ const required = new Set(Array.isArray(schema?.required) ? schema.required : []);
580
+ const properties = schema?.properties || {};
581
+ const shape = {};
582
+ for (const [name, propertySchema] of Object.entries(properties)) {
583
+ let field = jsonSchemaPropertyToZod(propertySchema);
584
+ if (!required.has(name)) field = field.optional();
585
+ shape[name] = field;
586
+ }
587
+ return shape;
588
+ }
589
+ function jsonSchemaPropertyToZod(schema) {
590
+ let field;
591
+ if (Array.isArray(schema?.enum) && schema.enum.length > 0) {
592
+ const values = schema.enum.filter((value) => typeof value === "string");
593
+ field = values.length > 0 ? z.enum(values) : z.string();
594
+ } else {
595
+ switch (schema?.type) {
596
+ case "number":
597
+ case "integer":
598
+ field = z.number();
599
+ break;
600
+ case "boolean":
601
+ field = z.boolean();
602
+ break;
603
+ case "array":
604
+ field = z.array(z.unknown());
605
+ break;
606
+ case "object":
607
+ field = z.record(z.unknown());
608
+ break;
609
+ case "string":
610
+ default:
611
+ field = z.string();
612
+ break;
613
+ }
614
+ }
615
+ if (schema?.description && typeof field.describe === "function") {
616
+ field = field.describe(schema.description);
617
+ }
618
+ if (schema?.default !== void 0) {
619
+ field = field.default(schema.default);
620
+ }
621
+ return field;
622
+ }
623
+ async function resolveStdioMcpContext() {
624
+ const apiKey = process.env.FUNDTRACER_MCP_API_KEY;
625
+ if (!apiKey) {
626
+ throw new Error("FUNDTRACER_MCP_API_KEY environment variable not set");
627
+ }
628
+ return validateMcpApiKey(apiKey);
629
+ }
630
+
631
+ // src/mcp/stdio.ts
632
+ dotenv.config();
633
+ console.log = console.error.bind(console);
634
+ process.env.FUNDTRACER_MCP_DISABLE_LOGGING = process.env.FUNDTRACER_MCP_DISABLE_LOGGING || "1";
635
+ async function main() {
636
+ let firebaseAvailable = false;
637
+ try {
638
+ const { initializeFirebase } = await import("../firebase.js");
639
+ initializeFirebase();
640
+ firebaseAvailable = true;
641
+ console.error("[MCP] Firebase initialized");
642
+ } catch (err2) {
643
+ console.error("[MCP] Firebase not available \u2014 key validation will fail. Set Firebase credentials in env.");
644
644
  }
645
+ const server = createFundTracerMcpServer(resolveStdioMcpContext);
645
646
  const transport = new StdioServerTransport();
646
647
  await server.connect(transport);
648
+ process.stdin.resume();
647
649
  console.error("[MCP] FundTracer MCP server running on stdio");
648
650
  }
649
651
  main().catch((err2) => {
@@ -652,7 +654,6 @@ main().catch((err2) => {
652
654
  });
653
655
  process.on("SIGINT", async () => {
654
656
  console.error("[MCP] Shutting down...");
655
- const { McpServer: McpServer2 } = await import("@modelcontextprotocol/server");
656
657
  process.exit(0);
657
658
  });
658
659
  process.on("SIGTERM", async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fundtracer/mcp",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "description": "FundTracer MCP Server — blockchain analysis for AI assistants (Claude Desktop, Claude Code, Cursor, etc.)",
5
5
  "type": "module",
6
6
  "main": "fundtracer-mcp.js",
@@ -13,10 +13,11 @@
13
13
  ],
14
14
  "dependencies": {
15
15
  "@cfworker/json-schema": "^4.1.1",
16
- "@modelcontextprotocol/server": "^2.0.0-alpha.2",
16
+ "@modelcontextprotocol/sdk": "^1.29.0",
17
17
  "axios": "^1.7.0",
18
18
  "dotenv": "^16.3.1",
19
- "node-fetch": "^2.7.0"
19
+ "node-fetch": "^2.7.0",
20
+ "zod": "^3.25.76"
20
21
  },
21
22
  "keywords": ["mcp", "blockchain", "wallet-analysis", "fundtracer", "ai"],
22
23
  "license": "GPL-3.0",