@openpump/mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1691 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/server.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+
6
+ // src/tools/token-tools.ts
7
+ import { z } from "zod";
8
+
9
+ // src/lib/api-client.ts
10
+ function createApiClient(apiKey, baseUrl) {
11
+ const authHeader = "Bearer " + apiKey;
12
+ return {
13
+ async get(path) {
14
+ return fetch(baseUrl + path, {
15
+ headers: { Authorization: authHeader }
16
+ });
17
+ },
18
+ async post(path, body) {
19
+ return fetch(baseUrl + path, {
20
+ method: "POST",
21
+ headers: {
22
+ Authorization: authHeader,
23
+ "Content-Type": "application/json"
24
+ },
25
+ body: JSON.stringify(body)
26
+ });
27
+ }
28
+ };
29
+ }
30
+
31
+ // src/tools/token-tools.ts
32
+ function agentError(code, message, suggestion) {
33
+ return {
34
+ content: [
35
+ {
36
+ type: "text",
37
+ text: JSON.stringify({ error: true, code, message, suggestion })
38
+ }
39
+ ]
40
+ };
41
+ }
42
+ function detectImageMimeType(contentType, imageUrl) {
43
+ const validTypes = ["image/png", "image/jpeg", "image/jpg", "image/gif", "image/webp"];
44
+ if (contentType) {
45
+ const normalized = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
46
+ if (validTypes.includes(normalized)) {
47
+ return normalized;
48
+ }
49
+ }
50
+ const lower = imageUrl.toLowerCase();
51
+ if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
52
+ if (lower.endsWith(".gif")) return "image/gif";
53
+ if (lower.endsWith(".webp")) return "image/webp";
54
+ return "image/png";
55
+ }
56
+ function registerTokenTools(server, userContext, apiBaseUrl) {
57
+ server.tool(
58
+ "create-token",
59
+ [
60
+ "Create a new token on PumpFun with a bonding curve.",
61
+ "Uploads token metadata and image to IPFS then submits the creation transaction.",
62
+ "Returns the mint address and transaction signature on success.",
63
+ "Typical confirmation: 2-5 seconds.",
64
+ "Not available to US persons. Use at own risk."
65
+ ].join(" "),
66
+ {
67
+ walletId: z.string().describe("ID of the creator/dev wallet"),
68
+ name: z.string().min(1).max(32).describe("Token name (max 32 chars)"),
69
+ symbol: z.string().min(1).max(10).describe("Token ticker symbol (max 10 chars)"),
70
+ description: z.string().max(500).describe("Token description (max 500 chars)"),
71
+ imageUrl: z.string().url().describe("Publicly accessible image URL (will be fetched and uploaded to IPFS)"),
72
+ initialBuyAmountSol: z.number().min(0).optional().describe("Optional: SOL amount for dev initial buy at creation"),
73
+ twitter: z.string().optional().describe("Twitter handle (optional)"),
74
+ telegram: z.string().optional().describe("Telegram link (optional)"),
75
+ website: z.string().url().optional().describe("Website URL (optional)")
76
+ },
77
+ async ({ walletId, name, symbol, description, imageUrl, initialBuyAmountSol, twitter, telegram, website }) => {
78
+ const wallet = userContext.wallets.find((w) => w.id === walletId);
79
+ if (!wallet) {
80
+ return agentError(
81
+ "WALLET_NOT_FOUND",
82
+ `Wallet "${walletId}" not found for this account.`,
83
+ "Use list-wallets to see available wallet IDs."
84
+ );
85
+ }
86
+ let imageBase64;
87
+ let imageType;
88
+ try {
89
+ const imageRes = await fetch(imageUrl);
90
+ if (!imageRes.ok) {
91
+ return agentError(
92
+ "IMAGE_FETCH_FAILED",
93
+ `Failed to fetch image from URL "${imageUrl}" (HTTP ${imageRes.status.toString()}).`,
94
+ "Ensure the image URL is publicly accessible and returns a valid image."
95
+ );
96
+ }
97
+ const contentType = imageRes.headers.get("content-type");
98
+ imageType = detectImageMimeType(contentType, imageUrl);
99
+ const imageBuffer = await imageRes.arrayBuffer();
100
+ imageBase64 = Buffer.from(imageBuffer).toString("base64");
101
+ } catch (error) {
102
+ return agentError(
103
+ "IMAGE_FETCH_FAILED",
104
+ `Failed to fetch image: ${error instanceof Error ? error.message : String(error)}`,
105
+ "Ensure the image URL is publicly accessible."
106
+ );
107
+ }
108
+ const requestBody = {
109
+ walletIndex: wallet.index,
110
+ name,
111
+ symbol,
112
+ description,
113
+ imageBase64,
114
+ imageType
115
+ };
116
+ if (initialBuyAmountSol !== void 0 && initialBuyAmountSol > 0) {
117
+ requestBody["initialBuyAmountSol"] = initialBuyAmountSol;
118
+ }
119
+ if (twitter !== void 0) requestBody["twitter"] = twitter;
120
+ if (telegram !== void 0) requestBody["telegram"] = telegram;
121
+ if (website !== void 0) requestBody["website"] = website;
122
+ try {
123
+ const api = createApiClient(userContext.apiKey, apiBaseUrl);
124
+ const res = await api.post("/api/tokens/create", requestBody);
125
+ if (!res.ok) {
126
+ const errBody = await res.text();
127
+ return agentError(
128
+ "TOKEN_CREATION_FAILED",
129
+ `Token creation failed (HTTP ${res.status.toString()}): ${errBody}`,
130
+ "Check the wallet has sufficient SOL and try again."
131
+ );
132
+ }
133
+ const data = await res.json();
134
+ return {
135
+ content: [{ type: "text", text: JSON.stringify(data) }]
136
+ };
137
+ } catch (error) {
138
+ return agentError(
139
+ "API_ERROR",
140
+ `Token creation request failed: ${error instanceof Error ? error.message : String(error)}`,
141
+ "Try again in a few seconds."
142
+ );
143
+ }
144
+ }
145
+ );
146
+ }
147
+
148
+ // src/tools/trading-tools.ts
149
+ import { z as z2 } from "zod";
150
+ var DISCLAIMER = "Not available to US persons. Use at own risk.";
151
+ var PRIORITY_LEVEL_SCHEMA = z2.enum(["economy", "normal", "fast", "turbo"]).optional().default("normal").describe(
152
+ "Transaction priority tier. Maps to Jito tip floor percentiles: 'economy' (25th), 'normal' (50th EMA, default), 'fast' (75th), 'turbo' (95th). Higher = faster inclusion, higher fee."
153
+ );
154
+ var APPROX_TIP_LAMPORTS = {
155
+ economy: 1e3,
156
+ normal: 5e4,
157
+ fast: 2e5,
158
+ turbo: 1e6
159
+ };
160
+ var RICO_WARNING = "LEGAL DISCLAIMER: Coordinated bundle buying (wash trading / simultaneous multi-wallet purchase at token creation) may be subject to legal restrictions in your jurisdiction. A RICO lawsuit filed July 2025 is active against bundling services. By setting confirm=true you acknowledge awareness of these risks. " + DISCLAIMER;
161
+ function agentError2(code, message, suggestion) {
162
+ return {
163
+ content: [
164
+ {
165
+ type: "text",
166
+ text: JSON.stringify({ error: true, code, message, suggestion })
167
+ }
168
+ ]
169
+ };
170
+ }
171
+ async function fetchUpdatedBalance(api, walletId) {
172
+ try {
173
+ const res = await api.post(`/api/wallets/${walletId}/refresh-balance`, {});
174
+ if (!res.ok) return null;
175
+ const body = await res.json();
176
+ const d = body.data;
177
+ if (!d) return null;
178
+ return {
179
+ solBalance: d.solBalance ?? "0",
180
+ lamports: d.lamports ?? "0",
181
+ tokenBalances: d.tokenBalances ?? []
182
+ };
183
+ } catch {
184
+ return null;
185
+ }
186
+ }
187
+ var LAMPORTS_PER_SOL = 1e9;
188
+ function registerTradingTools(server, userContext, apiBaseUrl) {
189
+ server.tool(
190
+ "bundle-buy",
191
+ [
192
+ RICO_WARNING,
193
+ "Atomically create a new PumpFun token and execute coordinated buys from multiple wallets using Jito MEV bundles.",
194
+ "Bundle 1 (token creation + up to 3 buy wallets) is atomic and same-block guaranteed.",
195
+ "Additional buyers use separate bundles and are NOT guaranteed same-block execution.",
196
+ "Requires confirm: true to execute.",
197
+ "Always run estimate-bundle-cost before this tool to verify sufficient SOL balance.",
198
+ "Returns jobId for async tracking."
199
+ ].join(" "),
200
+ {
201
+ devWalletId: z2.string().describe("ID of the dev/creator wallet"),
202
+ buyWalletIds: z2.array(z2.string()).max(20).describe("IDs of wallets to participate in the bundle buy (max 20)"),
203
+ tokenParams: z2.object({
204
+ name: z2.string().max(32).describe("Token name (max 32 chars)"),
205
+ symbol: z2.string().max(10).describe("Token ticker symbol (max 10 chars)"),
206
+ description: z2.string().max(500).describe("Token description (max 500 chars)"),
207
+ imageUrl: z2.string().url().describe("Token image URL")
208
+ }),
209
+ devBuyAmountSol: z2.string().regex(/^\d+$/, "Must be a decimal integer string").describe('SOL amount for the dev wallet initial buy in lamports (decimal string, e.g. "100000000" = 0.1 SOL)'),
210
+ walletBuyAmounts: z2.array(z2.string().regex(/^\d+$/, "Must be a decimal integer string")).describe("SOL amount per wallet in lamports (decimal strings), same order as buyWalletIds"),
211
+ priorityLevel: PRIORITY_LEVEL_SCHEMA,
212
+ confirm: z2.boolean().describe(
213
+ "REQUIRED: Must be true to execute. Run estimate-bundle-cost first to see total SOL required."
214
+ )
215
+ },
216
+ async ({ devWalletId, buyWalletIds, tokenParams, devBuyAmountSol, walletBuyAmounts, priorityLevel, confirm }) => {
217
+ if (!confirm) {
218
+ return agentError2(
219
+ "CONFIRMATION_REQUIRED",
220
+ "bundle-buy requires explicit confirmation (confirm: true) before execution.",
221
+ "First run estimate-bundle-cost to see total SOL required. Then call bundle-buy again with confirm: true."
222
+ );
223
+ }
224
+ const devWallet = userContext.wallets.find((w) => w.id === devWalletId);
225
+ if (!devWallet) {
226
+ return agentError2(
227
+ "WALLET_NOT_FOUND",
228
+ `Dev wallet "${devWalletId}" not found for this account.`,
229
+ "Use list-wallets to see available wallet IDs."
230
+ );
231
+ }
232
+ const missingWallets = buyWalletIds.filter(
233
+ (id) => !userContext.wallets.some((w) => w.id === id)
234
+ );
235
+ if (missingWallets.length > 0) {
236
+ return agentError2(
237
+ "WALLET_NOT_FOUND",
238
+ `Buy wallets not found: ${missingWallets.join(", ")}.`,
239
+ "Use list-wallets to see available wallet IDs."
240
+ );
241
+ }
242
+ if (walletBuyAmounts.length !== buyWalletIds.length) {
243
+ return agentError2(
244
+ "INVALID_INPUT",
245
+ `walletBuyAmounts length (${walletBuyAmounts.length.toString()}) must match buyWalletIds length (${buyWalletIds.length.toString()}).`,
246
+ "Provide one SOL amount per wallet in buyWalletIds, in the same order."
247
+ );
248
+ }
249
+ try {
250
+ const api = createApiClient(userContext.apiKey, apiBaseUrl);
251
+ const imageRes = await fetch(tokenParams.imageUrl);
252
+ if (!imageRes.ok) {
253
+ return agentError2(
254
+ "IMAGE_FETCH_FAILED",
255
+ `Failed to fetch token image from ${tokenParams.imageUrl}: HTTP ${imageRes.status.toString()}`,
256
+ "Provide a publicly accessible image URL."
257
+ );
258
+ }
259
+ const imageBuffer = await imageRes.arrayBuffer();
260
+ const imageBase64 = Buffer.from(imageBuffer).toString("base64");
261
+ const contentType = imageRes.headers.get("content-type") ?? "image/png";
262
+ const ACCEPTED_MIME_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
263
+ const imageType = ACCEPTED_MIME_TYPES.find((m) => contentType.includes(m.split("/")[1] ?? "")) ?? "image/png";
264
+ const approxTipLamports = APPROX_TIP_LAMPORTS[priorityLevel] ?? APPROX_TIP_LAMPORTS["normal"] ?? 5e4;
265
+ const requestBody = {
266
+ devWalletId,
267
+ buyWalletIds,
268
+ name: tokenParams.name,
269
+ symbol: tokenParams.symbol,
270
+ description: tokenParams.description,
271
+ imageBase64,
272
+ imageType,
273
+ devBuyAmountLamports: devBuyAmountSol,
274
+ walletBuyAmounts,
275
+ tipLamports: approxTipLamports
276
+ };
277
+ const res = await api.post("/api/tokens/bundle-launch", requestBody);
278
+ if (!res.ok) {
279
+ const errBody = await res.text();
280
+ return agentError2(
281
+ "BUNDLE_LAUNCH_FAILED",
282
+ `Bundle launch failed (HTTP ${res.status.toString()}): ${errBody}`,
283
+ "Check wallet balances and try again."
284
+ );
285
+ }
286
+ const data = await res.json();
287
+ return {
288
+ content: [
289
+ {
290
+ type: "text",
291
+ text: JSON.stringify({
292
+ jobId: data.jobId,
293
+ message: "Bundle launch submitted. Use poll-job to track progress.",
294
+ note: RICO_WARNING
295
+ })
296
+ }
297
+ ]
298
+ };
299
+ } catch (error) {
300
+ return agentError2(
301
+ "API_ERROR",
302
+ `Bundle launch request failed: ${error instanceof Error ? error.message : String(error)}`,
303
+ "Try again in a few seconds."
304
+ );
305
+ }
306
+ }
307
+ );
308
+ server.tool(
309
+ "bundle-sell",
310
+ [
311
+ `Sell a PumpFun token from multiple wallets simultaneously using Jito MEV bundles.`,
312
+ "Groups as many wallet sell instructions as possible into each transaction (up to the 1232-byte Solana limit),",
313
+ "then packs transactions into Jito bundles (max 5 txs each). One Jito tip is paid per bundle.",
314
+ "Only supported for bonding curve tokens (not yet graduated to PumpSwap).",
315
+ "Returns bundle statuses and per-wallet warnings.",
316
+ DISCLAIMER
317
+ ].join(" "),
318
+ {
319
+ mint: z2.string().describe("Token mint address (base58)"),
320
+ walletSells: z2.array(
321
+ z2.object({
322
+ walletId: z2.string().describe("ID of the wallet holding the token"),
323
+ tokenAmount: z2.union([
324
+ z2.string().regex(/^\d+$/, "Must be a decimal integer string").describe('Raw token base units as a decimal string (e.g. "435541983646")'),
325
+ z2.literal("all")
326
+ ]).describe('Amount to sell as raw base units, or "all" to sell entire balance')
327
+ })
328
+ ).min(1).max(20).describe("Per-wallet sell amounts (1-20 wallets)"),
329
+ tipWalletId: z2.string().optional().describe("Wallet ID that pays the Jito tip (default: first wallet in walletSells)"),
330
+ slippageBps: z2.number().int().min(0).max(1e4).optional().describe("Slippage tolerance in basis points (default: 500 = 5%)"),
331
+ priorityLevel: PRIORITY_LEVEL_SCHEMA,
332
+ confirm: z2.boolean().describe("REQUIRED: Must be true to execute the bundle sell.")
333
+ },
334
+ async ({ mint, walletSells, tipWalletId, slippageBps, priorityLevel, confirm }) => {
335
+ if (!confirm) {
336
+ return agentError2(
337
+ "CONFIRMATION_REQUIRED",
338
+ "bundle-sell requires explicit confirmation (confirm: true) before execution.",
339
+ "Call bundle-sell again with confirm: true to proceed."
340
+ );
341
+ }
342
+ const missingWallets = walletSells.map((ws) => ws.walletId).filter((id) => !userContext.wallets.some((w) => w.id === id));
343
+ if (missingWallets.length > 0) {
344
+ return agentError2(
345
+ "WALLET_NOT_FOUND",
346
+ `Wallets not found: ${missingWallets.join(", ")}.`,
347
+ "Use list-wallets to see available wallet IDs."
348
+ );
349
+ }
350
+ if (tipWalletId !== void 0 && !userContext.wallets.some((w) => w.id === tipWalletId)) {
351
+ return agentError2(
352
+ "WALLET_NOT_FOUND",
353
+ `Tip wallet "${tipWalletId}" not found for this account.`,
354
+ "Use list-wallets to see available wallet IDs."
355
+ );
356
+ }
357
+ try {
358
+ const api = createApiClient(userContext.apiKey, apiBaseUrl);
359
+ const body = {
360
+ walletSells,
361
+ priorityLevel
362
+ };
363
+ if (tipWalletId !== void 0) body["tipWalletId"] = tipWalletId;
364
+ if (slippageBps !== void 0) body["slippageBps"] = slippageBps;
365
+ const res = await api.post(`/api/tokens/${mint}/bundle-sell`, body);
366
+ if (!res.ok) {
367
+ const errBody = await res.text();
368
+ return agentError2(
369
+ "BUNDLE_SELL_FAILED",
370
+ `Bundle sell failed (HTTP ${res.status.toString()}): ${errBody}`,
371
+ "Check wallet balances and that the token is still on the bonding curve."
372
+ );
373
+ }
374
+ const data = await res.json();
375
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
376
+ } catch (error) {
377
+ return agentError2(
378
+ "API_ERROR",
379
+ `Bundle sell request failed: ${error instanceof Error ? error.message : String(error)}`,
380
+ "Try again in a few seconds."
381
+ );
382
+ }
383
+ }
384
+ );
385
+ server.tool(
386
+ "buy-token",
387
+ `Buy a PumpFun token with SOL from the specified wallet. Submits a swap transaction on the bonding curve. Returns the transaction result directly. ${DISCLAIMER}`,
388
+ {
389
+ walletId: z2.string().describe("ID of the wallet to buy with"),
390
+ mint: z2.string().describe("Token mint address (base58)"),
391
+ amountSol: z2.string().regex(/^\d+$/, "Must be a decimal integer string").describe(
392
+ 'Amount of SOL to spend in lamports (decimal string, e.g. "100000000" = 0.1 SOL). IMPORTANT: use the exact integer string -- do NOT use floats or decimals.'
393
+ ),
394
+ slippageBps: z2.number().int().min(0).max(1e4).optional().describe("Slippage tolerance in basis points (default: 500 = 5%)"),
395
+ priorityLevel: PRIORITY_LEVEL_SCHEMA
396
+ },
397
+ async ({ walletId, mint, amountSol, slippageBps, priorityLevel }) => {
398
+ const wallet = userContext.wallets.find((w) => w.id === walletId);
399
+ if (!wallet) {
400
+ return agentError2(
401
+ "WALLET_NOT_FOUND",
402
+ `Wallet "${walletId}" not found.`,
403
+ "Use list-wallets to see available wallet IDs."
404
+ );
405
+ }
406
+ try {
407
+ const api = createApiClient(userContext.apiKey, apiBaseUrl);
408
+ const body = {
409
+ walletId,
410
+ amountLamports: amountSol,
411
+ // API expects lamports as decimal string
412
+ priorityLevel
413
+ };
414
+ if (slippageBps !== void 0) {
415
+ body["slippageBps"] = slippageBps;
416
+ }
417
+ const res = await api.post(`/api/tokens/${mint}/buy`, body);
418
+ if (res.status === 404) {
419
+ return agentError2(
420
+ "WALLET_NOT_FOUND",
421
+ `Wallet "${walletId}" could not be resolved by the API.`,
422
+ "Ensure the wallet keypair is configured on the server."
423
+ );
424
+ }
425
+ if (!res.ok) {
426
+ const errBody = await res.text();
427
+ return agentError2(
428
+ "BUY_FAILED",
429
+ `Buy transaction failed (HTTP ${res.status.toString()}): ${errBody}`,
430
+ "Check the wallet has sufficient SOL and try again."
431
+ );
432
+ }
433
+ const data = await res.json();
434
+ const updatedBalance = await fetchUpdatedBalance(api, walletId);
435
+ return {
436
+ content: [
437
+ {
438
+ type: "text",
439
+ text: JSON.stringify({ ...data, updatedWalletBalance: updatedBalance })
440
+ }
441
+ ]
442
+ };
443
+ } catch (error) {
444
+ return agentError2(
445
+ "API_ERROR",
446
+ `Buy request failed: ${error instanceof Error ? error.message : String(error)}`,
447
+ "Try again in a few seconds."
448
+ );
449
+ }
450
+ }
451
+ );
452
+ server.tool(
453
+ "sell-token",
454
+ `Sell a PumpFun token back to SOL from the specified wallet. Use tokenAmount: "all" to sell the entire balance. Returns the transaction result directly. ${DISCLAIMER}`,
455
+ {
456
+ walletId: z2.string().describe("ID of the wallet holding the token"),
457
+ mint: z2.string().describe("Token mint address (base58)"),
458
+ tokenAmount: z2.union([
459
+ z2.string().regex(/^\d+$/, "Must be a decimal integer string").describe(
460
+ 'Raw token base units as a decimal string (same as the "amount" field from get-wallet-balance/get-token-holdings, e.g. "435541983646"). IMPORTANT: use the exact string -- do NOT convert to a JS number.'
461
+ ),
462
+ z2.literal("all")
463
+ ]).describe('Raw token base units as a decimal string, or "all" to sell the entire balance. Use get-token-holdings to get the raw "amount" string for a specific wallet.'),
464
+ slippageBps: z2.number().int().min(0).max(1e4).optional().describe("Slippage tolerance in basis points (default: 500 = 5%)"),
465
+ priorityLevel: PRIORITY_LEVEL_SCHEMA
466
+ },
467
+ async ({ walletId, mint, tokenAmount, slippageBps, priorityLevel }) => {
468
+ const wallet = userContext.wallets.find((w) => w.id === walletId);
469
+ if (!wallet) {
470
+ return agentError2(
471
+ "WALLET_NOT_FOUND",
472
+ `Wallet "${walletId}" not found.`,
473
+ "Use list-wallets to see available wallet IDs."
474
+ );
475
+ }
476
+ try {
477
+ const api = createApiClient(userContext.apiKey, apiBaseUrl);
478
+ const body = {
479
+ walletId,
480
+ tokenAmount,
481
+ priorityLevel
482
+ };
483
+ if (slippageBps !== void 0) {
484
+ body["slippageBps"] = slippageBps;
485
+ }
486
+ const res = await api.post(`/api/tokens/${mint}/sell`, body);
487
+ if (res.status === 404) {
488
+ return agentError2(
489
+ "WALLET_NOT_FOUND",
490
+ `Wallet "${walletId}" could not be resolved by the API.`,
491
+ "Ensure the wallet keypair is configured on the server."
492
+ );
493
+ }
494
+ if (!res.ok) {
495
+ const errBody = await res.text();
496
+ return agentError2(
497
+ "SELL_FAILED",
498
+ `Sell transaction failed (HTTP ${res.status.toString()}): ${errBody}`,
499
+ "Check the wallet has sufficient token balance and try again."
500
+ );
501
+ }
502
+ const data = await res.json();
503
+ const updatedBalance = await fetchUpdatedBalance(api, walletId);
504
+ return {
505
+ content: [
506
+ {
507
+ type: "text",
508
+ text: JSON.stringify({ ...data, updatedWalletBalance: updatedBalance })
509
+ }
510
+ ]
511
+ };
512
+ } catch (error) {
513
+ return agentError2(
514
+ "API_ERROR",
515
+ `Sell request failed: ${error instanceof Error ? error.message : String(error)}`,
516
+ "Try again in a few seconds."
517
+ );
518
+ }
519
+ }
520
+ );
521
+ server.tool(
522
+ "estimate-bundle-cost",
523
+ [
524
+ "Estimate the total SOL required for a bundle launch before executing.",
525
+ "Run this before bundle-buy to verify sufficient wallet balances.",
526
+ "Returns a breakdown of tip, network fees, and buy amounts.",
527
+ DISCLAIMER
528
+ ].join(" "),
529
+ {
530
+ buyWalletCount: z2.number().int().min(1).max(20).describe("Number of buy wallets (max 20)"),
531
+ devBuyAmountSol: z2.string().regex(/^\d+$/, "Must be a decimal integer string").describe('Dev wallet buy amount in lamports (decimal string, e.g. "100000000" = 0.1 SOL)'),
532
+ walletBuyAmounts: z2.array(z2.string().regex(/^\d+$/, "Must be a decimal integer string")).describe("SOL amount per buy wallet in lamports (decimal strings), in the same order as buyWalletIds will be"),
533
+ tipLamports: z2.number().int().min(1e3).optional().describe("Jito MEV tip in lamports (default: 1,000,000 = 0.001 SOL)"),
534
+ priorityLevel: PRIORITY_LEVEL_SCHEMA
535
+ },
536
+ ({ buyWalletCount, devBuyAmountSol, walletBuyAmounts, tipLamports: customTipLamports, priorityLevel }) => {
537
+ const approxTipLamports = customTipLamports ?? APPROX_TIP_LAMPORTS[priorityLevel] ?? APPROX_TIP_LAMPORTS["normal"] ?? 5e4;
538
+ const tipBI = BigInt(approxTipLamports);
539
+ const TOKEN_CREATION_FEE_LAMPORTS = 20000000n;
540
+ const TOKEN_ACCOUNT_RENT_LAMPORTS2 = 2049280n;
541
+ const NETWORK_FEE_PER_TX_LAMPORTS2 = 5000n;
542
+ const txCount = BigInt(1 + buyWalletCount);
543
+ const networkFeesLamports = txCount * NETWORK_FEE_PER_TX_LAMPORTS2;
544
+ const devBuyLamports = BigInt(devBuyAmountSol);
545
+ const walletBuysLamports = walletBuyAmounts.reduce((sum, amt) => sum + BigInt(amt), 0n);
546
+ const rentLamports = BigInt(buyWalletCount + 1) * TOKEN_ACCOUNT_RENT_LAMPORTS2;
547
+ const totalLamports = tipBI + networkFeesLamports + devBuyLamports + walletBuysLamports + rentLamports + TOKEN_CREATION_FEE_LAMPORTS;
548
+ const warnings = [];
549
+ if (buyWalletCount > 3) {
550
+ warnings.push(
551
+ "Wallets beyond index 3 are NOT guaranteed same-block as creation (not atomic). Consider whether this is acceptable."
552
+ );
553
+ }
554
+ if (walletBuyAmounts.some((amt) => BigInt(amt) < 10000000n)) {
555
+ warnings.push(
556
+ "Some wallet buy amounts are below 10000000 lamports (0.01 SOL). Very small buys may fail due to minimum transaction size requirements."
557
+ );
558
+ }
559
+ return {
560
+ content: [
561
+ {
562
+ type: "text",
563
+ text: JSON.stringify({
564
+ totalLamports: totalLamports.toString(),
565
+ totalSolApprox: (Number(totalLamports) / LAMPORTS_PER_SOL).toFixed(9),
566
+ breakdown: {
567
+ tipCostLamports: tipBI.toString(),
568
+ networkFeesLamports: networkFeesLamports.toString(),
569
+ tokenCreationFeeLamports: TOKEN_CREATION_FEE_LAMPORTS.toString(),
570
+ devBuyLamports: devBuyLamports.toString(),
571
+ walletBuysLamports: walletBuysLamports.toString(),
572
+ tokenAccountRentsLamports: rentLamports.toString()
573
+ },
574
+ warnings,
575
+ note: `Estimates are approximate. Jito tip for '${priorityLevel}' tier uses ~${approxTipLamports.toLocaleString()} lamports (live values fetched at execution time may differ). Total fees may vary by +/-20% based on network congestion.`
576
+ })
577
+ }
578
+ ]
579
+ };
580
+ }
581
+ );
582
+ server.tool(
583
+ "claim-creator-fees",
584
+ [
585
+ "Claim all accumulated creator fees for a wallet address.",
586
+ "Fees are per creator wallet (covering ALL tokens launched from that address) -- one transaction claims everything.",
587
+ "Run get-creator-fees first to check the claimable balance.",
588
+ `Returns signature and amount claimed. ${DISCLAIMER}`
589
+ ].join(" "),
590
+ {
591
+ creatorAddress: z2.string().describe(
592
+ "Creator wallet address (base58) to claim fees for. Must be one of your platform wallets."
593
+ )
594
+ },
595
+ async ({ creatorAddress }) => {
596
+ const wallet = userContext.wallets.find((w) => w.publicKey === creatorAddress);
597
+ if (!wallet) {
598
+ return agentError2(
599
+ "WALLET_NOT_FOUND",
600
+ `Address "${creatorAddress}" is not one of your platform wallets.`,
601
+ "Use get-creator-fees (no address) to see all your wallets and their claimable fees."
602
+ );
603
+ }
604
+ try {
605
+ const api = createApiClient(userContext.apiKey, apiBaseUrl);
606
+ const res = await api.post("/api/creator-fees/claim", { creatorAddress });
607
+ if (!res.ok) {
608
+ const errBody = await res.text();
609
+ return agentError2(
610
+ "CLAIM_FEES_FAILED",
611
+ `Claim fees failed (HTTP ${res.status.toString()}): ${errBody}`,
612
+ "Ensure the wallet has accumulated fees. Run get-creator-fees to check."
613
+ );
614
+ }
615
+ const data = await res.json();
616
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
617
+ } catch (error) {
618
+ return agentError2(
619
+ "API_ERROR",
620
+ `Claim fees request failed: ${error instanceof Error ? error.message : String(error)}`,
621
+ "Try again in a few seconds."
622
+ );
623
+ }
624
+ }
625
+ );
626
+ }
627
+
628
+ // src/tools/transfer-tools.ts
629
+ import { z as z3 } from "zod";
630
+ var DISCLAIMER2 = "Not available to US persons. Use at own risk.";
631
+ var LAMPORTS_PER_SOL2 = 1e9;
632
+ var TRANSFER_CAP_LAMPORTS = 10000000000n;
633
+ var RENT_EXEMPT_MINIMUM_LAMPORTS = 1000000n;
634
+ var NETWORK_FEE_PER_TX_LAMPORTS = 5000n;
635
+ var TOKEN_ACCOUNT_RENT_LAMPORTS = 2049280n;
636
+ var TRANSFER_ANNOTATIONS = {
637
+ destructiveHint: true,
638
+ idempotentHint: false,
639
+ readOnlyHint: false,
640
+ openWorldHint: true
641
+ };
642
+ function agentError3(code, message, suggestion) {
643
+ return {
644
+ content: [
645
+ {
646
+ type: "text",
647
+ text: JSON.stringify({ error: true, code, message, suggestion })
648
+ }
649
+ ]
650
+ };
651
+ }
652
+ async function parseApiError(res) {
653
+ const text = await res.text();
654
+ try {
655
+ const parsed = JSON.parse(text);
656
+ return {
657
+ code: parsed.code ?? parsed.error ?? "TRANSFER_FAILED",
658
+ message: parsed.message ?? text
659
+ };
660
+ } catch {
661
+ return { code: "TRANSFER_FAILED", message: text };
662
+ }
663
+ }
664
+ function registerTransferTools(server, userContext, apiBaseUrl) {
665
+ server.tool(
666
+ "transfer-sol",
667
+ [
668
+ "Send SOL from a custodial wallet to any Solana address (internal or external).",
669
+ "Use get-wallet-balance before calling to verify sufficient balance.",
670
+ "Sender balance after transfer must remain above 0.001 SOL (Solana rent-exempt minimum).",
671
+ `Maximum transfer: ${TRANSFER_CAP_LAMPORTS.toString()} lamports (10 SOL) per call.`,
672
+ "Use dryRun: true to validate and estimate fees without submitting.",
673
+ "Requires confirm: true to execute.",
674
+ DISCLAIMER2
675
+ ].join(" "),
676
+ {
677
+ fromWalletId: z3.string().describe("ID of the source wallet (from list-wallets)"),
678
+ toAddress: z3.string().describe(
679
+ "Destination Solana address (base58). Accepts any valid address -- internal wallet public keys or external addresses."
680
+ ),
681
+ amountSol: z3.string().regex(/^\d+$/, "Must be a decimal integer string").describe(
682
+ `Amount of SOL to send in lamports (decimal string, e.g. "500000000" = 0.5 SOL). Maximum ${TRANSFER_CAP_LAMPORTS.toString()} lamports (10 SOL) per call.`
683
+ ),
684
+ memo: z3.string().max(256).optional().describe("Optional on-chain memo attached to the transaction (max 256 chars)."),
685
+ priorityFeeMicroLamports: z3.number().int().min(0).max(1e6).optional().describe("Priority fee in micro-lamports per compute unit. Omit to use the API default."),
686
+ dryRun: z3.boolean().optional().default(false).describe("If true, validates inputs and estimates fees without submitting. confirm is not required."),
687
+ confirm: z3.boolean().describe(
688
+ "REQUIRED: Must be true to execute. Run with dryRun: true first to preview the transfer."
689
+ )
690
+ },
691
+ TRANSFER_ANNOTATIONS,
692
+ async ({ fromWalletId, toAddress, amountSol, memo, priorityFeeMicroLamports, dryRun, confirm }) => {
693
+ const fromWallet = userContext.wallets.find((w) => w.id === fromWalletId);
694
+ if (!fromWallet) {
695
+ return agentError3(
696
+ "WALLET_NOT_FOUND",
697
+ `Source wallet "${fromWalletId}" not found for this account.`,
698
+ "Use list-wallets to see available wallet IDs."
699
+ );
700
+ }
701
+ if (fromWallet.publicKey === toAddress) {
702
+ return agentError3(
703
+ "INVALID_INPUT",
704
+ "Cannot transfer SOL to the same wallet (fromWalletId and toAddress resolve to the same public key).",
705
+ "Provide a different destination address."
706
+ );
707
+ }
708
+ const amountLamports = BigInt(amountSol);
709
+ if (amountLamports > TRANSFER_CAP_LAMPORTS) {
710
+ return agentError3(
711
+ "INVALID_INPUT",
712
+ `amountSol (${amountLamports.toString()} lamports) exceeds maximum of ${TRANSFER_CAP_LAMPORTS.toString()} lamports (10 SOL) per call.`,
713
+ "Split into multiple smaller transfers."
714
+ );
715
+ }
716
+ if (fromWallet.solBalance !== void 0) {
717
+ const balanceLamports = BigInt(Math.round(fromWallet.solBalance * LAMPORTS_PER_SOL2));
718
+ const remainingLamports = balanceLamports - amountLamports - NETWORK_FEE_PER_TX_LAMPORTS;
719
+ if (remainingLamports < RENT_EXEMPT_MINIMUM_LAMPORTS) {
720
+ return agentError3(
721
+ "INSUFFICIENT_BALANCE",
722
+ `Transfer would leave ${remainingLamports.toString()} lamports in the source wallet, below the Solana rent-exempt minimum of ${RENT_EXEMPT_MINIMUM_LAMPORTS.toString()} lamports. Current balance: ${balanceLamports.toString()} lamports.`,
723
+ "Reduce the transfer amount to keep at least 1000000 lamports (0.001 SOL) in the source wallet."
724
+ );
725
+ }
726
+ }
727
+ if (dryRun) {
728
+ const balanceLamports = fromWallet.solBalance === void 0 ? null : BigInt(Math.round(fromWallet.solBalance * LAMPORTS_PER_SOL2));
729
+ const remainingLamports = balanceLamports === null ? null : balanceLamports - amountLamports - NETWORK_FEE_PER_TX_LAMPORTS;
730
+ return {
731
+ content: [
732
+ {
733
+ type: "text",
734
+ text: JSON.stringify({
735
+ dryRun: true,
736
+ valid: true,
737
+ amountLamports: amountLamports.toString(),
738
+ fromAddress: fromWallet.publicKey,
739
+ toAddress,
740
+ estimatedNetworkFeeLamports: NETWORK_FEE_PER_TX_LAMPORTS.toString(),
741
+ remainingLamportsAfterTransfer: remainingLamports?.toString() ?? null,
742
+ rentExemptWarning: remainingLamports !== null && remainingLamports < RENT_EXEMPT_MINIMUM_LAMPORTS,
743
+ message: "Transfer would succeed. Call again with confirm: true to execute."
744
+ })
745
+ }
746
+ ]
747
+ };
748
+ }
749
+ if (!confirm) {
750
+ return agentError3(
751
+ "CONFIRMATION_REQUIRED",
752
+ "transfer-sol requires confirm: true to execute.",
753
+ "Run with dryRun: true to preview the transfer, then call again with confirm: true."
754
+ );
755
+ }
756
+ try {
757
+ const api = createApiClient(userContext.apiKey, apiBaseUrl);
758
+ const body = {
759
+ toAddress,
760
+ amountLamports: amountSol
761
+ // already a lamports decimal string
762
+ };
763
+ if (memo !== void 0) body["memo"] = memo;
764
+ if (priorityFeeMicroLamports !== void 0)
765
+ body["priorityFeeMicroLamports"] = priorityFeeMicroLamports;
766
+ const res = await api.post(`/api/wallets/${fromWalletId}/transfer`, body);
767
+ if (!res.ok) {
768
+ const { code, message } = await parseApiError(res);
769
+ return agentError3(
770
+ code,
771
+ `SOL transfer failed (HTTP ${res.status.toString()}): ${message}`,
772
+ "Verify the destination address is valid and the wallet has sufficient SOL."
773
+ );
774
+ }
775
+ const data = await res.json();
776
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
777
+ } catch (error) {
778
+ return agentError3(
779
+ "API_ERROR",
780
+ `Transfer request failed: ${error instanceof Error ? error.message : String(error)}`,
781
+ "Try again in a few seconds."
782
+ );
783
+ }
784
+ }
785
+ );
786
+ server.tool(
787
+ "transfer-token",
788
+ [
789
+ "Send SPL tokens from a custodial wallet to any Solana address (internal or external).",
790
+ 'tokenAmount is the raw base-unit amount string (same as the "amount" field from get-token-holdings), or "all" to send the entire balance.',
791
+ "If the destination lacks a token account for this mint, the transaction creates one (~0.002 SOL rent, paid by the sender).",
792
+ "Use dryRun: true to validate and estimate fees without submitting.",
793
+ "Requires confirm: true to execute.",
794
+ DISCLAIMER2
795
+ ].join(" "),
796
+ {
797
+ fromWalletId: z3.string().describe("ID of the source wallet holding the token (from list-wallets)"),
798
+ toAddress: z3.string().describe(
799
+ "Destination Solana address (base58). Accepts any valid address -- internal wallet public keys or external addresses."
800
+ ),
801
+ mint: z3.string().describe("SPL token mint address (base58)"),
802
+ tokenAmount: z3.union([
803
+ z3.string().regex(/^\d+$/, "Must be a non-negative integer string").describe(
804
+ 'Raw token base units as a string (same format as get-token-holdings "amount" field, e.g. "1000000" for 1 token with 6 decimals)'
805
+ ),
806
+ z3.literal("all")
807
+ ]).describe(
808
+ 'Raw token base units as a decimal string, or "all" to transfer the entire balance. Use get-token-holdings to get the raw "amount" string for a specific wallet.'
809
+ ),
810
+ memo: z3.string().max(256).optional().describe("Optional on-chain memo attached to the transaction (max 256 chars)."),
811
+ priorityFeeMicroLamports: z3.number().int().min(0).max(1e6).optional().describe("Priority fee in micro-lamports per compute unit. Omit to use the API default."),
812
+ dryRun: z3.boolean().optional().default(false).describe("If true, validates inputs and estimates fees without submitting."),
813
+ confirm: z3.boolean().describe("REQUIRED: Must be true to execute the token transfer.")
814
+ },
815
+ TRANSFER_ANNOTATIONS,
816
+ async ({
817
+ fromWalletId,
818
+ toAddress,
819
+ mint,
820
+ tokenAmount,
821
+ memo,
822
+ priorityFeeMicroLamports,
823
+ dryRun,
824
+ confirm
825
+ }) => {
826
+ const fromWallet = userContext.wallets.find((w) => w.id === fromWalletId);
827
+ if (!fromWallet) {
828
+ return agentError3(
829
+ "WALLET_NOT_FOUND",
830
+ `Source wallet "${fromWalletId}" not found for this account.`,
831
+ "Use list-wallets to see available wallet IDs."
832
+ );
833
+ }
834
+ if (fromWallet.publicKey === toAddress) {
835
+ return agentError3(
836
+ "INVALID_INPUT",
837
+ "Cannot transfer tokens to the same wallet.",
838
+ "Provide a different destination address."
839
+ );
840
+ }
841
+ const api = createApiClient(userContext.apiKey, apiBaseUrl);
842
+ let resolvedAmountLamports;
843
+ if (tokenAmount === "all") {
844
+ try {
845
+ const balRes = await api.get(`/api/wallets/${fromWalletId}/balance`);
846
+ if (!balRes.ok) {
847
+ return agentError3(
848
+ "API_ERROR",
849
+ `Failed to fetch balance to resolve "all" (HTTP ${balRes.status.toString()}).`,
850
+ "Use get-wallet-balance to check holdings, then pass the raw amount string directly."
851
+ );
852
+ }
853
+ const balData = await balRes.json();
854
+ const entry = balData.data.tokenBalances.find((tb) => tb.mint === mint);
855
+ if (!entry || entry.amount === "0") {
856
+ return agentError3(
857
+ "NO_TOKEN_BALANCE",
858
+ `Wallet "${fromWalletId}" holds no balance of token ${mint}.`,
859
+ "Use get-token-holdings to check which wallets hold this token."
860
+ );
861
+ }
862
+ resolvedAmountLamports = entry.amount;
863
+ } catch (error) {
864
+ return agentError3(
865
+ "API_ERROR",
866
+ `Balance fetch failed: ${error instanceof Error ? error.message : String(error)}`,
867
+ "Try again in a few seconds."
868
+ );
869
+ }
870
+ } else {
871
+ resolvedAmountLamports = tokenAmount;
872
+ }
873
+ if (dryRun) {
874
+ return {
875
+ content: [
876
+ {
877
+ type: "text",
878
+ text: JSON.stringify({
879
+ dryRun: true,
880
+ valid: true,
881
+ fromAddress: fromWallet.publicKey,
882
+ toAddress,
883
+ mint,
884
+ tokenAmountRaw: resolvedAmountLamports,
885
+ estimatedNetworkFeeLamports: NETWORK_FEE_PER_TX_LAMPORTS.toString(),
886
+ tokenAccountCreationCostLamports: TOKEN_ACCOUNT_RENT_LAMPORTS.toString(),
887
+ message: "Token transfer would proceed. If destination lacks a token account, ~0.002 SOL rent will be deducted from the source wallet. Call again with confirm: true to execute."
888
+ })
889
+ }
890
+ ]
891
+ };
892
+ }
893
+ if (!confirm) {
894
+ return agentError3(
895
+ "CONFIRMATION_REQUIRED",
896
+ "transfer-token requires confirm: true to execute.",
897
+ "Run with dryRun: true to preview the transfer, then call again with confirm: true."
898
+ );
899
+ }
900
+ try {
901
+ const body = {
902
+ toAddress,
903
+ amountLamports: resolvedAmountLamports,
904
+ mint
905
+ };
906
+ if (memo !== void 0) body["memo"] = memo;
907
+ if (priorityFeeMicroLamports !== void 0)
908
+ body["priorityFeeMicroLamports"] = priorityFeeMicroLamports;
909
+ const res = await api.post(`/api/wallets/${fromWalletId}/transfer`, body);
910
+ if (!res.ok) {
911
+ const { code, message } = await parseApiError(res);
912
+ return agentError3(
913
+ code,
914
+ `Token transfer failed (HTTP ${res.status.toString()}): ${message}`,
915
+ "Verify the destination address is valid and the wallet holds sufficient token balance."
916
+ );
917
+ }
918
+ const data = await res.json();
919
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
920
+ } catch (error) {
921
+ return agentError3(
922
+ "API_ERROR",
923
+ `Transfer request failed: ${error instanceof Error ? error.message : String(error)}`,
924
+ "Try again in a few seconds."
925
+ );
926
+ }
927
+ }
928
+ );
929
+ }
930
+
931
+ // src/tools/wallet-tools.ts
932
+ import { z as z4 } from "zod";
933
+ var DISCLAIMER3 = "Not available to US persons. Use at own risk.";
934
+ function agentError4(code, message, suggestion) {
935
+ return {
936
+ content: [
937
+ {
938
+ type: "text",
939
+ text: JSON.stringify({ error: true, code, message, suggestion })
940
+ }
941
+ ]
942
+ };
943
+ }
944
+ function registerWalletTools(server, userContext, apiBaseUrl) {
945
+ server.tool(
946
+ "create-wallet",
947
+ [
948
+ "Create a new HD-derived custodial wallet for this account.",
949
+ "The wallet is generated from the account master seed using BIP44 derivation (Phantom-compatible).",
950
+ "Returns the new wallet ID, public key, and derivation index.",
951
+ "Use list-wallets after creation to see the updated wallet list.",
952
+ DISCLAIMER3
953
+ ].join(" "),
954
+ {
955
+ label: z4.string().max(100).optional().describe('Optional human-readable label for the wallet (e.g. "sniper-1", "launch-wallet").')
956
+ },
957
+ async ({ label }) => {
958
+ try {
959
+ const api = createApiClient(userContext.apiKey, apiBaseUrl);
960
+ const body = {};
961
+ if (label !== void 0) body["label"] = label;
962
+ const res = await api.post("/api/wallets", body);
963
+ if (!res.ok) {
964
+ const text = await res.text();
965
+ return agentError4(
966
+ "CREATE_WALLET_FAILED",
967
+ `Failed to create wallet (HTTP ${res.status.toString()}): ${text}`,
968
+ "Try again in a few seconds."
969
+ );
970
+ }
971
+ const data = await res.json();
972
+ if (data?.data?.id && data.data.publicKey !== void 0) {
973
+ userContext.wallets.push({
974
+ id: data.data.id,
975
+ publicKey: data.data.publicKey,
976
+ label: data.data.label ?? null,
977
+ index: data.data.walletIndex ?? userContext.wallets.length
978
+ });
979
+ }
980
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
981
+ } catch (error) {
982
+ return agentError4(
983
+ "API_ERROR",
984
+ `Create wallet request failed: ${error instanceof Error ? error.message : String(error)}`,
985
+ "Try again in a few seconds."
986
+ );
987
+ }
988
+ }
989
+ );
990
+ server.tool(
991
+ "get-aggregate-balance",
992
+ [
993
+ "Get the total SOL balance across all wallets in this account.",
994
+ "Returns totalSol, totalLamports, and walletCount.",
995
+ "Use this to quickly check how much SOL is available across all wallets before a bundle buy or large operation.",
996
+ DISCLAIMER3
997
+ ].join(" "),
998
+ {},
999
+ // No parameters -- aggregates all wallets for the authenticated user
1000
+ async () => {
1001
+ try {
1002
+ const api = createApiClient(userContext.apiKey, apiBaseUrl);
1003
+ const res = await api.get("/api/wallets/aggregate-balance");
1004
+ if (!res.ok) {
1005
+ const text = await res.text();
1006
+ return agentError4(
1007
+ "API_ERROR",
1008
+ `Failed to fetch aggregate balance (HTTP ${res.status.toString()}): ${text}`,
1009
+ "Try again in a few seconds."
1010
+ );
1011
+ }
1012
+ const data = await res.json();
1013
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
1014
+ } catch (error) {
1015
+ return agentError4(
1016
+ "API_ERROR",
1017
+ `Aggregate balance request failed: ${error instanceof Error ? error.message : String(error)}`,
1018
+ "Try again in a few seconds."
1019
+ );
1020
+ }
1021
+ }
1022
+ );
1023
+ server.tool(
1024
+ "get-wallet-deposit-address",
1025
+ [
1026
+ "Get the deposit address and funding instructions for a custodial wallet.",
1027
+ "Returns the public key (deposit address), minimum SOL amounts for common operations, and instructions for sending SOL from an external wallet.",
1028
+ "Use this when you need to tell the user how to fund a wallet from Phantom, Solflare, or any other external source.",
1029
+ DISCLAIMER3
1030
+ ].join(" "),
1031
+ {
1032
+ walletId: z4.string().describe("ID of the wallet to get the deposit address for")
1033
+ },
1034
+ async ({ walletId }) => {
1035
+ const wallet = userContext.wallets.find((w) => w.id === walletId);
1036
+ if (!wallet) {
1037
+ return agentError4(
1038
+ "WALLET_NOT_FOUND",
1039
+ `Wallet "${walletId}" not found for this account.`,
1040
+ "Use list-wallets to see available wallet IDs."
1041
+ );
1042
+ }
1043
+ try {
1044
+ const api = createApiClient(userContext.apiKey, apiBaseUrl);
1045
+ const res = await api.get(`/api/wallets/${walletId}/deposit-instructions`);
1046
+ if (!res.ok) {
1047
+ const text = await res.text();
1048
+ return agentError4(
1049
+ "API_ERROR",
1050
+ `Failed to fetch deposit instructions (HTTP ${res.status.toString()}): ${text}`,
1051
+ "Try again in a few seconds."
1052
+ );
1053
+ }
1054
+ const data = await res.json();
1055
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
1056
+ } catch (error) {
1057
+ return agentError4(
1058
+ "API_ERROR",
1059
+ `Deposit instructions request failed: ${error instanceof Error ? error.message : String(error)}`,
1060
+ "Try again in a few seconds."
1061
+ );
1062
+ }
1063
+ }
1064
+ );
1065
+ server.tool(
1066
+ "get-wallet-transactions",
1067
+ [
1068
+ "Get the paginated transfer history for a wallet.",
1069
+ "Returns buy, sell, and transfer transactions ordered newest-first.",
1070
+ "Use type filter to narrow to a specific transaction type.",
1071
+ "Use limit and offset for pagination (max 100 per page).",
1072
+ DISCLAIMER3
1073
+ ].join(" "),
1074
+ {
1075
+ walletId: z4.string().describe("ID of the wallet to fetch transaction history for"),
1076
+ type: z4.enum(["buy", "sell", "transfer"]).optional().describe("Filter by transaction type. Omit to return all types."),
1077
+ limit: z4.number().int().min(1).max(100).optional().default(50).describe("Number of transactions to return (default 50, max 100)."),
1078
+ offset: z4.number().int().min(0).optional().default(0).describe("Number of transactions to skip for pagination (default 0).")
1079
+ },
1080
+ async ({ walletId, type, limit, offset }) => {
1081
+ const wallet = userContext.wallets.find((w) => w.id === walletId);
1082
+ if (!wallet) {
1083
+ return agentError4(
1084
+ "WALLET_NOT_FOUND",
1085
+ `Wallet "${walletId}" not found for this account.`,
1086
+ "Use list-wallets to see available wallet IDs."
1087
+ );
1088
+ }
1089
+ try {
1090
+ const api = createApiClient(userContext.apiKey, apiBaseUrl);
1091
+ const params = new URLSearchParams({
1092
+ limit: limit.toString(),
1093
+ offset: offset.toString()
1094
+ });
1095
+ if (type !== void 0) params.set("type", type);
1096
+ const res = await api.get(`/api/wallets/${walletId}/transactions?${params.toString()}`);
1097
+ if (!res.ok) {
1098
+ const text = await res.text();
1099
+ return agentError4(
1100
+ "API_ERROR",
1101
+ `Failed to fetch transactions (HTTP ${res.status.toString()}): ${text}`,
1102
+ "Try again in a few seconds."
1103
+ );
1104
+ }
1105
+ const data = await res.json();
1106
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
1107
+ } catch (error) {
1108
+ return agentError4(
1109
+ "API_ERROR",
1110
+ `Transactions request failed: ${error instanceof Error ? error.message : String(error)}`,
1111
+ "Try again in a few seconds."
1112
+ );
1113
+ }
1114
+ }
1115
+ );
1116
+ }
1117
+
1118
+ // src/tools/info-tools.ts
1119
+ import { z as z5 } from "zod";
1120
+ var DISCLAIMER4 = "Not available to US persons. Use at own risk.";
1121
+ function agentError5(code, message, suggestion) {
1122
+ return {
1123
+ content: [
1124
+ {
1125
+ type: "text",
1126
+ text: JSON.stringify({ error: true, code, message, suggestion })
1127
+ }
1128
+ ]
1129
+ };
1130
+ }
1131
+ function registerInfoTools(server, userContext, apiBaseUrl) {
1132
+ server.tool(
1133
+ "get-token-info",
1134
+ `Get current info about a PumpFun token: name, symbol, price, market cap, bonding curve progress, and graduation status. This is a public read -- no authentication required. ${DISCLAIMER4}`,
1135
+ {
1136
+ mint: z5.string().describe("Token mint address (base58)")
1137
+ },
1138
+ async ({ mint }) => {
1139
+ try {
1140
+ const api = createApiClient(userContext.apiKey, apiBaseUrl);
1141
+ const res = await api.get(`/api/tokens/${mint}/curve-state`);
1142
+ if (res.status === 404) {
1143
+ return agentError5(
1144
+ "TOKEN_NOT_FOUND",
1145
+ `Token with mint "${mint}" was not found on PumpFun.`,
1146
+ "Verify the mint address is correct and the token exists on pump.fun."
1147
+ );
1148
+ }
1149
+ if (!res.ok) {
1150
+ const errBody = await res.text();
1151
+ return agentError5(
1152
+ "API_ERROR",
1153
+ `Failed to fetch token info (HTTP ${res.status.toString()}): ${errBody}`,
1154
+ "Try again in a few seconds. If the error persists, the API may be unavailable."
1155
+ );
1156
+ }
1157
+ const data = await res.json();
1158
+ return {
1159
+ content: [{ type: "text", text: JSON.stringify(data) }]
1160
+ };
1161
+ } catch (error) {
1162
+ return agentError5(
1163
+ "RPC_ERROR",
1164
+ `Failed to fetch token info: ${error instanceof Error ? error.message : String(error)}`,
1165
+ "Try again in a few seconds. If the error persists, the Solana RPC may be degraded."
1166
+ );
1167
+ }
1168
+ }
1169
+ );
1170
+ server.tool(
1171
+ "get-token-market-info",
1172
+ [
1173
+ "Get rich market analytics for any Solana token: price (SOL + USD), market cap, 24h volume, buy/sell counts, price change percentages, and risk metrics (snipers, bundlers, insiders).",
1174
+ "Mainnet only -- returns null data on devnet.",
1175
+ "Use this before deciding when to sell: high sniper count or unusual price action may signal a rug.",
1176
+ DISCLAIMER4
1177
+ ].join(" "),
1178
+ {
1179
+ mint: z5.string().describe("Token mint address (base58)")
1180
+ },
1181
+ async ({ mint }) => {
1182
+ try {
1183
+ const api = createApiClient(userContext.apiKey, apiBaseUrl);
1184
+ const res = await api.get(`/api/tokens/${mint}/market-info`);
1185
+ if (!res.ok) {
1186
+ const errBody = await res.text();
1187
+ return agentError5(
1188
+ "API_ERROR",
1189
+ `Failed to fetch market info (HTTP ${res.status.toString()}): ${errBody}`,
1190
+ "Try again in a few seconds."
1191
+ );
1192
+ }
1193
+ const data = await res.json();
1194
+ return {
1195
+ content: [{ type: "text", text: JSON.stringify(data) }]
1196
+ };
1197
+ } catch (error) {
1198
+ return agentError5(
1199
+ "API_ERROR",
1200
+ `Market info request failed: ${error instanceof Error ? error.message : String(error)}`,
1201
+ "Try again in a few seconds."
1202
+ );
1203
+ }
1204
+ }
1205
+ );
1206
+ server.tool(
1207
+ "list-my-tokens",
1208
+ [
1209
+ "List all tokens launched by the authenticated user.",
1210
+ "Returns mint address, name, symbol, graduation status (active/graduated), metadata URI, and creation timestamp.",
1211
+ "Combine with get-token-market-info to enrich each token with live price and volume data.",
1212
+ DISCLAIMER4
1213
+ ].join(" "),
1214
+ {},
1215
+ // No parameters -- scoped to authenticated user automatically
1216
+ async () => {
1217
+ try {
1218
+ const api = createApiClient(userContext.apiKey, apiBaseUrl);
1219
+ const res = await api.get("/api/tokens");
1220
+ if (!res.ok) {
1221
+ const errBody = await res.text();
1222
+ return agentError5(
1223
+ "API_ERROR",
1224
+ `Failed to fetch token list (HTTP ${res.status.toString()}): ${errBody}`,
1225
+ "Try again in a few seconds."
1226
+ );
1227
+ }
1228
+ const data = await res.json();
1229
+ return {
1230
+ content: [{ type: "text", text: JSON.stringify(data) }]
1231
+ };
1232
+ } catch (error) {
1233
+ return agentError5(
1234
+ "API_ERROR",
1235
+ `Token list request failed: ${error instanceof Error ? error.message : String(error)}`,
1236
+ "Try again in a few seconds."
1237
+ );
1238
+ }
1239
+ }
1240
+ );
1241
+ server.tool(
1242
+ "get-token-holdings",
1243
+ [
1244
+ "Check which of the user's platform wallets hold a specific token, and how much.",
1245
+ "Omit mint to see ALL tokens held across every wallet -- useful when you know the symbol/name but not the mint address.",
1246
+ "Provide mint to filter to that specific token only.",
1247
+ "Returns wallets with a positive balance.",
1248
+ "Use this before sell-token to know which walletIds and amounts to sell.",
1249
+ DISCLAIMER4
1250
+ ].join(" "),
1251
+ {
1252
+ mint: z5.string().optional().describe("Token mint address (base58) to check holdings for. Omit to return ALL token holdings across all wallets.")
1253
+ },
1254
+ async ({ mint }) => {
1255
+ const wallets = userContext.wallets;
1256
+ if (wallets.length === 0) {
1257
+ return {
1258
+ content: [
1259
+ {
1260
+ type: "text",
1261
+ text: JSON.stringify({ holdings: [], note: "No platform wallets configured." })
1262
+ }
1263
+ ]
1264
+ };
1265
+ }
1266
+ const api = createApiClient(userContext.apiKey, apiBaseUrl);
1267
+ const balanceResults = await Promise.allSettled(
1268
+ wallets.map(async (w) => {
1269
+ const res = await api.get(`/api/wallets/${w.id}/balance`);
1270
+ if (!res.ok) throw new Error(`HTTP ${res.status.toString()} for wallet ${w.id}`);
1271
+ const body = await res.json();
1272
+ return { wallet: w, tokenBalances: body.data.tokenBalances };
1273
+ })
1274
+ );
1275
+ if (mint !== void 0) {
1276
+ const holdings = [];
1277
+ for (const result of balanceResults) {
1278
+ if (result.status !== "fulfilled") continue;
1279
+ const { wallet, tokenBalances } = result.value;
1280
+ const entry = tokenBalances.find((tb) => tb.mint === mint);
1281
+ if (!entry) continue;
1282
+ const hasBalance = entry.uiAmount === null ? entry.amount !== "0" : entry.uiAmount > 0;
1283
+ if (!hasBalance) continue;
1284
+ holdings.push({
1285
+ walletId: wallet.id,
1286
+ walletLabel: wallet.label ?? "",
1287
+ publicKey: wallet.publicKey,
1288
+ amount: entry.amount,
1289
+ uiAmount: entry.uiAmount,
1290
+ decimals: entry.decimals
1291
+ });
1292
+ }
1293
+ return {
1294
+ content: [
1295
+ {
1296
+ type: "text",
1297
+ text: JSON.stringify({
1298
+ mint,
1299
+ holdings,
1300
+ totalHoldingWallets: holdings.length,
1301
+ note: holdings.length === 0 ? "No platform wallets hold this token." : `${holdings.length.toString()} wallet(s) hold this token. Pass the "amount" string directly to sell-token as tokenAmount to sell the exact on-chain balance with no float rounding.`
1302
+ })
1303
+ }
1304
+ ]
1305
+ };
1306
+ }
1307
+ const allHoldings = [];
1308
+ for (const result of balanceResults) {
1309
+ if (result.status !== "fulfilled") continue;
1310
+ const { wallet, tokenBalances } = result.value;
1311
+ for (const entry of tokenBalances) {
1312
+ const hasBalance = entry.uiAmount === null ? entry.amount !== "0" : entry.uiAmount > 0;
1313
+ if (!hasBalance) continue;
1314
+ allHoldings.push({
1315
+ mint: entry.mint,
1316
+ walletId: wallet.id,
1317
+ walletLabel: wallet.label ?? "",
1318
+ publicKey: wallet.publicKey,
1319
+ amount: entry.amount,
1320
+ uiAmount: entry.uiAmount,
1321
+ decimals: entry.decimals
1322
+ });
1323
+ }
1324
+ }
1325
+ return {
1326
+ content: [
1327
+ {
1328
+ type: "text",
1329
+ text: JSON.stringify({
1330
+ holdings: allHoldings,
1331
+ totalTokenPositions: allHoldings.length,
1332
+ note: allHoldings.length === 0 ? "No token holdings found across any platform wallet." : `${allHoldings.length.toString()} token position(s) found. Use the "mint" field with sell-token, and "amount" as tokenAmount for exact on-chain sells with no float rounding.`
1333
+ })
1334
+ }
1335
+ ]
1336
+ };
1337
+ }
1338
+ );
1339
+ server.tool(
1340
+ "get-wallet-balance",
1341
+ `Get the SOL balance and all token balances held by the specified wallet. Returns real-time on-chain data. ${DISCLAIMER4}`,
1342
+ {
1343
+ walletId: z5.string().describe("ID of the wallet to check balance for")
1344
+ },
1345
+ async ({ walletId }) => {
1346
+ const wallet = userContext.wallets.find((w) => w.id === walletId);
1347
+ if (!wallet) {
1348
+ return agentError5(
1349
+ "WALLET_NOT_FOUND",
1350
+ `Wallet "${walletId}" not found for this account.`,
1351
+ "Use list-wallets to see available wallet IDs and their public keys."
1352
+ );
1353
+ }
1354
+ try {
1355
+ const api = createApiClient(userContext.apiKey, apiBaseUrl);
1356
+ const res = await api.get(`/api/wallets/${walletId}/balance`);
1357
+ if (res.status === 404) {
1358
+ return agentError5(
1359
+ "WALLET_NOT_FOUND",
1360
+ `Wallet "${walletId}" not found or not accessible.`,
1361
+ "Use list-wallets to see available wallet IDs."
1362
+ );
1363
+ }
1364
+ if (!res.ok) {
1365
+ const errBody = await res.text();
1366
+ return agentError5(
1367
+ "API_ERROR",
1368
+ `Failed to fetch wallet balance (HTTP ${res.status.toString()}): ${errBody}`,
1369
+ "Try again in a few seconds."
1370
+ );
1371
+ }
1372
+ const data = await res.json();
1373
+ return {
1374
+ content: [{ type: "text", text: JSON.stringify(data) }]
1375
+ };
1376
+ } catch (error) {
1377
+ return agentError5(
1378
+ "RPC_ERROR",
1379
+ `Failed to fetch balance for wallet ${wallet.publicKey}: ${error instanceof Error ? error.message : String(error)}`,
1380
+ "Try again in a few seconds."
1381
+ );
1382
+ }
1383
+ }
1384
+ );
1385
+ server.tool(
1386
+ "get-creator-fees",
1387
+ [
1388
+ "Check accumulated PumpFun creator fees for one or all wallets.",
1389
+ "Fees accumulate in a single creator vault per wallet address, covering ALL tokens launched from that wallet.",
1390
+ "Omit address to check fees across all of your platform wallets at once.",
1391
+ "Provide an address to check a specific creator (including wallets not on this platform).",
1392
+ DISCLAIMER4
1393
+ ].join(" "),
1394
+ {
1395
+ address: z5.string().optional().describe(
1396
+ "Creator wallet address (base58) to check fees for. Omit to check all your wallets."
1397
+ )
1398
+ },
1399
+ async ({ address }) => {
1400
+ const api = createApiClient(userContext.apiKey, apiBaseUrl);
1401
+ if (address) {
1402
+ try {
1403
+ const res = await api.get(`/api/creator-fees?address=${address}`);
1404
+ if (!res.ok) {
1405
+ const errBody = await res.text();
1406
+ return agentError5(
1407
+ "API_ERROR",
1408
+ `Failed to fetch creator fees (HTTP ${res.status.toString()}): ${errBody}`,
1409
+ "Verify the address is a valid base58 Solana public key."
1410
+ );
1411
+ }
1412
+ const data = await res.json();
1413
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
1414
+ } catch (error) {
1415
+ return agentError5(
1416
+ "API_ERROR",
1417
+ `Creator fees request failed: ${error instanceof Error ? error.message : String(error)}`,
1418
+ "Try again in a few seconds."
1419
+ );
1420
+ }
1421
+ }
1422
+ const wallets = userContext.wallets;
1423
+ if (wallets.length === 0) {
1424
+ return {
1425
+ content: [
1426
+ {
1427
+ type: "text",
1428
+ text: JSON.stringify({
1429
+ wallets: [],
1430
+ totalAccumulatedSOL: 0,
1431
+ note: "No platform wallets found."
1432
+ })
1433
+ }
1434
+ ]
1435
+ };
1436
+ }
1437
+ const results = await Promise.allSettled(
1438
+ wallets.map(async (w) => {
1439
+ const res = await api.get(`/api/creator-fees?address=${w.publicKey}`);
1440
+ if (!res.ok) throw new Error(`HTTP ${res.status.toString()} for ${w.publicKey}`);
1441
+ return {
1442
+ wallet: w,
1443
+ fees: await res.json()
1444
+ };
1445
+ })
1446
+ );
1447
+ const walletFees = results.filter((r) => r.status === "fulfilled").map(({ value: { wallet, fees } }) => ({
1448
+ walletId: wallet.id,
1449
+ publicKey: wallet.publicKey,
1450
+ label: wallet.label,
1451
+ accumulatedLamports: fees.accumulatedLamports,
1452
+ accumulatedSOL: fees.accumulatedSOL,
1453
+ creatorVaultAddress: fees.creatorVaultAddress
1454
+ }));
1455
+ const totalAccumulatedSOL = walletFees.reduce((sum, w) => sum + w.accumulatedSOL, 0);
1456
+ return {
1457
+ content: [
1458
+ {
1459
+ type: "text",
1460
+ text: JSON.stringify({
1461
+ wallets: walletFees,
1462
+ totalAccumulatedSOL,
1463
+ note: totalAccumulatedSOL > 0 ? `${totalAccumulatedSOL.toFixed(9)} SOL claimable. Use claim-creator-fees with the relevant creatorAddress.` : "No pending creator fees across any of your wallets."
1464
+ })
1465
+ }
1466
+ ]
1467
+ };
1468
+ }
1469
+ );
1470
+ server.tool(
1471
+ "list-wallets",
1472
+ `List all wallets belonging to the authenticated user, including their public keys, labels, and derivation index. Use get-wallet-balance for live SOL and token balances. ${DISCLAIMER4}`,
1473
+ {},
1474
+ // No parameters -- returns all wallets for the authenticated user
1475
+ () => {
1476
+ const wallets = userContext.wallets.map((w) => ({
1477
+ walletId: w.id,
1478
+ publicKey: w.publicKey,
1479
+ label: w.label,
1480
+ walletIndex: w.index,
1481
+ solBalance: w.solBalance ?? null
1482
+ }));
1483
+ return {
1484
+ content: [{ type: "text", text: JSON.stringify(wallets) }]
1485
+ };
1486
+ }
1487
+ );
1488
+ server.tool(
1489
+ "get-token-quote",
1490
+ [
1491
+ "Get a price quote for buying or selling a PumpFun token without submitting a transaction.",
1492
+ 'For buy: set action="buy" and solAmount (SOL to spend in lamports) -> returns expectedTokens.',
1493
+ 'For sell: set action="sell" and tokenAmount (raw base units, from get-token-holdings) -> returns expectedSol.',
1494
+ "Also returns route (bonding_curve or pumpswap), priceImpact %, and fee in basis points.",
1495
+ "Use this before buy-token or sell-token to preview the trade.",
1496
+ DISCLAIMER4
1497
+ ].join(" "),
1498
+ {
1499
+ mint: z5.string().describe("Token mint address (base58)"),
1500
+ action: z5.enum(["buy", "sell"]).describe('"buy" to quote a purchase, "sell" to quote a sale'),
1501
+ solAmount: z5.string().regex(/^\d+$/, "Must be a decimal integer string").optional().describe(
1502
+ 'SOL to spend on a buy in lamports (decimal string, e.g. "100000000" = 0.1 SOL). Required when action="buy".'
1503
+ ),
1504
+ tokenAmount: z5.string().regex(/^\d+$/, "Must be a decimal integer string").optional().describe(
1505
+ 'Raw token base units to sell as a decimal string (same as the "amount" field from get-token-holdings, e.g. "435541983646"). Required when action="sell".'
1506
+ )
1507
+ },
1508
+ async ({ mint, action, solAmount, tokenAmount }) => {
1509
+ if (action === "buy" && solAmount === void 0) {
1510
+ return agentError5(
1511
+ "MISSING_PARAM",
1512
+ 'solAmount is required when action="buy".',
1513
+ 'Set solAmount to the SOL you want to spend in lamports (e.g. "100000000" = 0.1 SOL).'
1514
+ );
1515
+ }
1516
+ if (action === "sell" && tokenAmount === void 0) {
1517
+ return agentError5(
1518
+ "MISSING_PARAM",
1519
+ 'tokenAmount is required when action="sell".',
1520
+ 'Set tokenAmount to the raw base-unit string from get-token-holdings (e.g. "435541983646").'
1521
+ );
1522
+ }
1523
+ try {
1524
+ const api = createApiClient(userContext.apiKey, apiBaseUrl);
1525
+ const params = new URLSearchParams({ action });
1526
+ if (action === "buy") {
1527
+ params.set("solAmount", solAmount);
1528
+ } else {
1529
+ params.set("tokenAmount", tokenAmount);
1530
+ }
1531
+ const res = await api.get(`/api/tokens/${mint}/quote?${params.toString()}`);
1532
+ if (!res.ok) {
1533
+ const text = await res.text();
1534
+ return agentError5(
1535
+ "QUOTE_FAILED",
1536
+ `Quote failed (HTTP ${res.status.toString()}): ${text}`,
1537
+ "Verify the mint address is correct and try again."
1538
+ );
1539
+ }
1540
+ const data = await res.json();
1541
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
1542
+ } catch (error) {
1543
+ return agentError5(
1544
+ "API_ERROR",
1545
+ `Quote request failed: ${error instanceof Error ? error.message : String(error)}`,
1546
+ "Try again in a few seconds."
1547
+ );
1548
+ }
1549
+ }
1550
+ );
1551
+ server.tool(
1552
+ "get-jito-tip-levels",
1553
+ [
1554
+ "Get current Jito MEV bundle tip amounts in lamports for each priority level (economy, normal, fast, turbo).",
1555
+ "Values are refreshed every 20 seconds from the Jito tip percentile API.",
1556
+ "Use this to pick an appropriate tipLamports for bundle-buy or to understand current MEV costs.",
1557
+ DISCLAIMER4
1558
+ ].join(" "),
1559
+ {},
1560
+ // No parameters -- public market data
1561
+ async () => {
1562
+ try {
1563
+ const api = createApiClient(userContext.apiKey, apiBaseUrl);
1564
+ const res = await api.get("/api/jito/tip");
1565
+ if (!res.ok) {
1566
+ const text = await res.text();
1567
+ return agentError5(
1568
+ "API_ERROR",
1569
+ `Failed to fetch Jito tip levels (HTTP ${res.status.toString()}): ${text}`,
1570
+ "Try again in a few seconds."
1571
+ );
1572
+ }
1573
+ const data = await res.json();
1574
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
1575
+ } catch (error) {
1576
+ return agentError5(
1577
+ "API_ERROR",
1578
+ `Jito tip levels request failed: ${error instanceof Error ? error.message : String(error)}`,
1579
+ "Try again in a few seconds."
1580
+ );
1581
+ }
1582
+ }
1583
+ );
1584
+ }
1585
+
1586
+ // src/tools/job-tools.ts
1587
+ import { z as z6 } from "zod";
1588
+ function registerJobTools(server, userContext, apiBaseUrl) {
1589
+ server.tool(
1590
+ "poll-job",
1591
+ 'Check the status of an async operation (create-token, bundle-buy, buy-token, sell-token, claim-creator-fees, transfer-sol, transfer-token). Call repeatedly until status is "completed" or "failed". Suggested polling interval: 2 seconds. Jobs expire after 10 minutes.',
1592
+ {
1593
+ jobId: z6.string().describe("Job ID returned by a previous async tool call")
1594
+ },
1595
+ async ({ jobId }) => {
1596
+ try {
1597
+ const api = createApiClient(userContext.apiKey, apiBaseUrl);
1598
+ const res = await api.get(`/api/jobs/${jobId}`);
1599
+ if (res.status === 404) {
1600
+ return {
1601
+ content: [
1602
+ {
1603
+ type: "text",
1604
+ text: JSON.stringify({
1605
+ error: true,
1606
+ code: "JOB_NOT_FOUND",
1607
+ message: `Job "${jobId}" was not found. It may have expired or the ID is incorrect.`,
1608
+ suggestion: "Verify the jobId from a recent async tool call. Jobs expire after 10 minutes."
1609
+ })
1610
+ }
1611
+ ]
1612
+ };
1613
+ }
1614
+ if (!res.ok) {
1615
+ const errBody = await res.text();
1616
+ return {
1617
+ content: [
1618
+ {
1619
+ type: "text",
1620
+ text: JSON.stringify({
1621
+ error: true,
1622
+ code: "API_ERROR",
1623
+ message: `Failed to fetch job status (HTTP ${res.status.toString()}): ${errBody}`,
1624
+ suggestion: "Try again in a few seconds."
1625
+ })
1626
+ }
1627
+ ]
1628
+ };
1629
+ }
1630
+ const data = await res.json();
1631
+ const response = {
1632
+ jobId: data.jobId,
1633
+ status: data.status,
1634
+ progress: data.progress,
1635
+ result: data.result ?? null,
1636
+ warnings: data.warnings,
1637
+ error: data.error ?? null
1638
+ };
1639
+ if (data.status === "pending" || data.status === "running" || data.status === "active" || data.status === "waiting") {
1640
+ response.hint = "Job is still processing. Poll again in 2 seconds.";
1641
+ }
1642
+ return {
1643
+ content: [
1644
+ {
1645
+ type: "text",
1646
+ text: JSON.stringify(response)
1647
+ }
1648
+ ]
1649
+ };
1650
+ } catch (error) {
1651
+ return {
1652
+ content: [
1653
+ {
1654
+ type: "text",
1655
+ text: JSON.stringify({
1656
+ error: true,
1657
+ code: "API_ERROR",
1658
+ message: `Job poll request failed: ${error instanceof Error ? error.message : String(error)}`,
1659
+ suggestion: "Try again in a few seconds."
1660
+ })
1661
+ }
1662
+ ]
1663
+ };
1664
+ }
1665
+ }
1666
+ );
1667
+ }
1668
+
1669
+ // src/tools/index.ts
1670
+ function registerAllTools(server, userContext, apiBaseUrl) {
1671
+ registerTokenTools(server, userContext, apiBaseUrl);
1672
+ registerTradingTools(server, userContext, apiBaseUrl);
1673
+ registerTransferTools(server, userContext, apiBaseUrl);
1674
+ registerWalletTools(server, userContext, apiBaseUrl);
1675
+ registerInfoTools(server, userContext, apiBaseUrl);
1676
+ registerJobTools(server, userContext, apiBaseUrl);
1677
+ }
1678
+
1679
+ // src/server.ts
1680
+ function createMcpServer(userContext, apiBaseUrl) {
1681
+ const server = new McpServer({
1682
+ name: "openpump",
1683
+ version: "1.0.0"
1684
+ });
1685
+ registerAllTools(server, userContext, apiBaseUrl);
1686
+ return server;
1687
+ }
1688
+
1689
+ export {
1690
+ createMcpServer
1691
+ };