helius-mcp 0.2.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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +150 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +29 -0
  5. package/dist/tools/accounts.d.ts +2 -0
  6. package/dist/tools/accounts.js +197 -0
  7. package/dist/tools/assets-extended.d.ts +2 -0
  8. package/dist/tools/assets-extended.js +54 -0
  9. package/dist/tools/assets.d.ts +2 -0
  10. package/dist/tools/assets.js +156 -0
  11. package/dist/tools/balance.d.ts +2 -0
  12. package/dist/tools/balance.js +169 -0
  13. package/dist/tools/config.d.ts +2 -0
  14. package/dist/tools/config.js +33 -0
  15. package/dist/tools/das.d.ts +2 -0
  16. package/dist/tools/das.js +428 -0
  17. package/dist/tools/enhanced-transactions.d.ts +2 -0
  18. package/dist/tools/enhanced-transactions.js +51 -0
  19. package/dist/tools/enhanced-websockets.d.ts +12 -0
  20. package/dist/tools/enhanced-websockets.js +329 -0
  21. package/dist/tools/index.d.ts +2 -0
  22. package/dist/tools/index.js +18 -0
  23. package/dist/tools/laserstream.d.ts +16 -0
  24. package/dist/tools/laserstream.js +348 -0
  25. package/dist/tools/priority-fees.d.ts +2 -0
  26. package/dist/tools/priority-fees.js +61 -0
  27. package/dist/tools/rpc.d.ts +2 -0
  28. package/dist/tools/rpc.js +130 -0
  29. package/dist/tools/shared.d.ts +6 -0
  30. package/dist/tools/shared.js +13 -0
  31. package/dist/tools/transactions-enhanced.d.ts +2 -0
  32. package/dist/tools/transactions-enhanced.js +165 -0
  33. package/dist/tools/transactions.d.ts +2 -0
  34. package/dist/tools/transactions.js +336 -0
  35. package/dist/tools/webhooks.d.ts +2 -0
  36. package/dist/tools/webhooks.js +156 -0
  37. package/dist/utils/formatters.d.ts +4 -0
  38. package/dist/utils/formatters.js +12 -0
  39. package/dist/utils/helius.d.ts +33 -0
  40. package/dist/utils/helius.js +134 -0
  41. package/package.json +50 -0
@@ -0,0 +1,169 @@
1
+ import { z } from 'zod';
2
+ import { getHeliusClient, hasApiKey } from '../utils/helius.js';
3
+ import { formatSol, formatAddress } from '../utils/formatters.js';
4
+ import { noApiKeyResponse } from './shared.js';
5
+ export function registerBalanceTools(server) {
6
+ // Get SOL Balance
7
+ server.tool('getBalance', 'Get native SOL balance for a Solana wallet address. Returns balance in both SOL and lamports (1 SOL = 1 billion lamports). Use this for checking how much SOL a wallet has. For token balances, use getTokenBalances instead.', {
8
+ address: z.string().describe('Solana wallet address (base58 encoded, e.g. Gh4tdJhLP1s55xGfghHHvPNPPrNtaDjc6dzZJ374DGHJ)')
9
+ }, async ({ address }) => {
10
+ if (!hasApiKey())
11
+ return noApiKeyResponse();
12
+ const helius = getHeliusClient();
13
+ const balance = await helius.getBalance(address);
14
+ const lamports = Number(balance.value);
15
+ return {
16
+ content: [{
17
+ type: 'text',
18
+ text: `**SOL Balance for ${formatAddress(address)}**\n\n${formatSol(lamports)} (${lamports.toLocaleString()} lamports)`
19
+ }]
20
+ };
21
+ });
22
+ // Get Token Balances
23
+ server.tool('getTokenBalances', 'Get all SPL token balances for a Solana wallet with full token info: names, symbols, properly formatted amounts with decimals, and USD prices (when available). Includes fungible tokens like USDC, BONK, JUP, etc. Does NOT include NFTs — use getAssetsByOwner for NFTs. For native SOL balance, use getBalance instead. Automatically paginates to fetch all tokens using parallel requests for speed.', {
24
+ address: z.string().describe('Solana wallet address (base58 encoded)')
25
+ }, async ({ address }) => {
26
+ if (!hasApiKey())
27
+ return noApiKeyResponse();
28
+ const helius = getHeliusClient();
29
+ const PAGE_SIZE = 10;
30
+ const MAX_ASSETS = 500;
31
+ const MAX_PAGES = 100;
32
+ // Helper to fetch a single page with retry on "too big" errors
33
+ async function fetchPage(page, limit = PAGE_SIZE) {
34
+ let currentLimit = limit;
35
+ while (currentLimit >= 1) {
36
+ try {
37
+ const response = await helius.getAssetsByOwner({
38
+ ownerAddress: address,
39
+ page,
40
+ limit: currentLimit,
41
+ displayOptions: { showFungible: true }
42
+ });
43
+ const items = response.items || [];
44
+ return { items, hasMore: items.length === currentLimit };
45
+ }
46
+ catch (err) {
47
+ const errorMsg = err instanceof Error ? err.message : String(err);
48
+ if (errorMsg.includes('too big') || errorMsg.includes('Response is too big')) {
49
+ if (currentLimit > 5)
50
+ currentLimit = 5;
51
+ else if (currentLimit > 2)
52
+ currentLimit = 2;
53
+ else if (currentLimit > 1)
54
+ currentLimit = 1;
55
+ else
56
+ return { items: [], hasMore: false };
57
+ }
58
+ else {
59
+ throw err;
60
+ }
61
+ }
62
+ }
63
+ return { items: [], hasMore: false };
64
+ }
65
+ // Step 1: Fetch first page to probe
66
+ const firstResult = await fetchPage(1);
67
+ const allAssets = [...firstResult.items];
68
+ if (allAssets.length === 0) {
69
+ return {
70
+ content: [{
71
+ type: 'text',
72
+ text: `**Token Balances for ${formatAddress(address)}**\n\nNo tokens found.`
73
+ }]
74
+ };
75
+ }
76
+ // Step 2: If first page is full, fetch remaining pages in parallel
77
+ // Strategy: fetch pages 2-15 in parallel (covers most wallets), then continue if needed
78
+ if (firstResult.hasMore && allAssets.length < MAX_ASSETS) {
79
+ let currentPage = 2;
80
+ let hasMorePages = true;
81
+ const BATCH_SIZE = 15; // Conservative batch size
82
+ while (hasMorePages && allAssets.length < MAX_ASSETS && currentPage <= MAX_PAGES) {
83
+ const batchPages = Array.from({ length: Math.min(BATCH_SIZE, MAX_PAGES - currentPage + 1) }, (_, i) => currentPage + i);
84
+ const results = await Promise.allSettled(batchPages.map(page => fetchPage(page)));
85
+ let pagesWithData = 0;
86
+ let lastPageWithData = -1;
87
+ for (let i = 0; i < results.length; i++) {
88
+ const result = results[i];
89
+ if (result.status === 'fulfilled' && result.value.items.length > 0) {
90
+ allAssets.push(...result.value.items);
91
+ pagesWithData++;
92
+ lastPageWithData = i;
93
+ }
94
+ }
95
+ // Stop if no pages had data, or if we found empty pages after data (reached end)
96
+ hasMorePages = pagesWithData > 0 && lastPageWithData === batchPages.length - 1;
97
+ currentPage += batchPages.length;
98
+ }
99
+ }
100
+ const fungibleTokens = allAssets.filter((asset) => asset.interface === 'FungibleToken' || asset.interface === 'FungibleAsset');
101
+ if (fungibleTokens.length === 0) {
102
+ return {
103
+ content: [{
104
+ type: 'text',
105
+ text: `**Token Balances for ${formatAddress(address)}**\n\nNo fungible tokens found.`
106
+ }]
107
+ };
108
+ }
109
+ // Enrich tokens missing name/symbol by fetching full asset data
110
+ const unknownTokens = fungibleTokens.filter((asset) => !asset.token_info?.symbol && !asset.content?.metadata?.symbol && !asset.content?.metadata?.name);
111
+ if (unknownTokens.length > 0 && unknownTokens.length <= 10) {
112
+ const enrichedData = await Promise.allSettled(unknownTokens.map((asset) => helius.getAsset({ id: asset.id })));
113
+ enrichedData.forEach((result, idx) => {
114
+ if (result.status === 'fulfilled' && result.value) {
115
+ const enriched = result.value;
116
+ const original = unknownTokens[idx];
117
+ if (enriched.content?.metadata) {
118
+ original.content = original.content || {};
119
+ original.content.metadata = enriched.content.metadata;
120
+ }
121
+ if (enriched.token_info?.symbol) {
122
+ original.token_info = original.token_info || {};
123
+ original.token_info.symbol = enriched.token_info.symbol;
124
+ }
125
+ }
126
+ });
127
+ }
128
+ const lines = [];
129
+ let totalUsdValue = 0;
130
+ let tokensWithPrice = 0;
131
+ fungibleTokens.forEach((asset) => {
132
+ const symbol = asset.token_info?.symbol || asset.content?.metadata?.symbol;
133
+ const name = asset.content?.metadata?.name;
134
+ const decimals = asset.token_info?.decimals ?? 0;
135
+ const balance = asset.token_info?.balance ?? 0;
136
+ const totalPrice = asset.token_info?.price_info?.total_price;
137
+ const formattedAmount = decimals > 0
138
+ ? (balance / Math.pow(10, decimals)).toLocaleString(undefined, { maximumFractionDigits: Math.min(decimals, 6) })
139
+ : balance.toLocaleString();
140
+ const displayName = symbol || name || formatAddress(asset.id);
141
+ let line = `- **${displayName}**: ${formattedAmount}`;
142
+ if (totalPrice !== undefined && totalPrice > 0) {
143
+ totalUsdValue += totalPrice;
144
+ tokensWithPrice++;
145
+ line += ` ($${totalPrice.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })})`;
146
+ }
147
+ if (!symbol && !name) {
148
+ line += ` — ${formatAddress(asset.id)}`;
149
+ }
150
+ lines.push(line);
151
+ });
152
+ let header = `**Token Balances for ${formatAddress(address)}** (${fungibleTokens.length} tokens)`;
153
+ if (allAssets.length >= MAX_ASSETS) {
154
+ header += ` — showing first ${MAX_ASSETS} assets`;
155
+ }
156
+ if (totalUsdValue > 0) {
157
+ header += `\n**Total Value:** $${totalUsdValue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
158
+ if (tokensWithPrice < fungibleTokens.length) {
159
+ header += ` (${tokensWithPrice}/${fungibleTokens.length} tokens have price data)`;
160
+ }
161
+ }
162
+ return {
163
+ content: [{
164
+ type: 'text',
165
+ text: `${header}\n\n${lines.join('\n')}`
166
+ }]
167
+ };
168
+ });
169
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerConfigTools(server: McpServer): void;
@@ -0,0 +1,33 @@
1
+ import { z } from 'zod';
2
+ import { setApiKey, setNetwork, rpcRequest } from '../utils/helius.js';
3
+ export function registerConfigTools(server) {
4
+ server.tool('setHeliusApiKey', 'Configure the Helius API key for this session. Required before using any blockchain data tools. Get your API key from https://dashboard.helius.dev/api-keys. The key is validated by making a test request.', {
5
+ apiKey: z.string().describe('Your Helius API key'),
6
+ network: z.enum(['mainnet-beta', 'devnet']).optional().default('mainnet-beta').describe('Network to use (default: mainnet-beta)')
7
+ }, async ({ apiKey, network }) => {
8
+ try {
9
+ setApiKey(apiKey);
10
+ if (network) {
11
+ setNetwork(network);
12
+ }
13
+ // Validate with test request
14
+ await rpcRequest('getBlockHeight');
15
+ return {
16
+ content: [{
17
+ type: 'text',
18
+ text: `✅ Helius API key configured for ${network}!\n\nYou can now query the Solana blockchain.`
19
+ }]
20
+ };
21
+ }
22
+ catch (err) {
23
+ const errorMsg = err instanceof Error ? err.message : String(err);
24
+ return {
25
+ content: [{
26
+ type: 'text',
27
+ text: `❌ Failed to configure API key: ${errorMsg}\n\nPlease check your key at https://dashboard.helius.dev/api-keys`
28
+ }],
29
+ isError: true
30
+ };
31
+ }
32
+ });
33
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerDasTools(server: McpServer): void;
@@ -0,0 +1,428 @@
1
+ import { z } from 'zod';
2
+ import { dasRequest } from '../utils/helius.js';
3
+ import { formatAddress } from '../utils/formatters.js';
4
+ export function registerDasTools(server) {
5
+ // getAsset - Get single asset details
6
+ server.tool('getAsset', 'Get comprehensive information about any Solana NFT, token, or compressed NFT by its mint address. Returns metadata, owner, creators, authorities, royalties, supply, and price data (for top 10k tokens by 24h volume). Supports all token standards including cNFTs and pNFTs.', {
7
+ id: z.string().describe('Asset mint address (base58 encoded)')
8
+ }, async ({ id }) => {
9
+ try {
10
+ const asset = await dasRequest('getAsset', { id });
11
+ const lines = [
12
+ `**${asset.content?.metadata?.name || asset.token_info?.symbol || 'Asset'}**`,
13
+ '',
14
+ `**Mint:** ${formatAddress(asset.id)}`,
15
+ `**Type:** ${asset.interface}`,
16
+ ];
17
+ if (asset.ownership?.owner) {
18
+ lines.push(`**Owner:** ${formatAddress(asset.ownership.owner)}`);
19
+ }
20
+ if (asset.content?.metadata?.symbol || asset.token_info?.symbol) {
21
+ lines.push(`**Symbol:** ${asset.content?.metadata?.symbol || asset.token_info?.symbol}`);
22
+ }
23
+ if (asset.creators?.length > 0) {
24
+ lines.push('', '**Creators:**');
25
+ asset.creators.forEach((c) => {
26
+ lines.push(`- ${formatAddress(c.address)} (${c.share}%) ${c.verified ? '✓' : ''}`);
27
+ });
28
+ }
29
+ if (asset.token_info) {
30
+ lines.push('', '**Token Info:**');
31
+ if (asset.token_info.supply) {
32
+ const supply = asset.token_info.decimals
33
+ ? (asset.token_info.supply / Math.pow(10, asset.token_info.decimals)).toLocaleString()
34
+ : asset.token_info.supply.toLocaleString();
35
+ lines.push(`- Supply: ${supply}`);
36
+ }
37
+ if (asset.token_info.decimals !== undefined) {
38
+ lines.push(`- Decimals: ${asset.token_info.decimals}`);
39
+ }
40
+ if (asset.token_info.price_info?.price_per_token) {
41
+ lines.push(`- Price: $${asset.token_info.price_info.price_per_token.toFixed(6)}`);
42
+ }
43
+ }
44
+ return {
45
+ content: [{ type: 'text', text: lines.join('\n') }]
46
+ };
47
+ }
48
+ catch (err) {
49
+ const errorMsg = err instanceof Error ? err.message : String(err);
50
+ return {
51
+ content: [{ type: 'text', text: `❌ Error: ${errorMsg}` }],
52
+ isError: true
53
+ };
54
+ }
55
+ });
56
+ // getAssetBatch - Fetch multiple assets (up to 1000)
57
+ server.tool('getAssetBatch', 'Fetch multiple assets in a single request (up to 1000 assets). Much faster than calling getAsset multiple times. Returns full asset data for each mint address provided.', {
58
+ ids: z.array(z.string()).describe('Array of asset mint addresses (up to 1000)')
59
+ }, async ({ ids }) => {
60
+ try {
61
+ if (ids.length > 1000) {
62
+ return {
63
+ content: [{ type: 'text', text: '❌ Maximum 1000 assets per batch' }],
64
+ isError: true
65
+ };
66
+ }
67
+ const assets = await dasRequest('getAssetBatch', { ids });
68
+ const lines = [`**Batch Assets** (${assets.length} assets)`, ''];
69
+ assets.slice(0, 20).forEach((asset, i) => {
70
+ const name = asset.content?.metadata?.name || asset.token_info?.symbol || formatAddress(asset.id);
71
+ lines.push(`${i + 1}. **${name}** (${asset.interface})`);
72
+ });
73
+ if (assets.length > 20) {
74
+ lines.push(`\n... and ${assets.length - 20} more assets`);
75
+ }
76
+ return {
77
+ content: [{ type: 'text', text: lines.join('\n') }]
78
+ };
79
+ }
80
+ catch (err) {
81
+ const errorMsg = err instanceof Error ? err.message : String(err);
82
+ return {
83
+ content: [{ type: 'text', text: `❌ Error: ${errorMsg}` }],
84
+ isError: true
85
+ };
86
+ }
87
+ });
88
+ // getAssetsByOwner - Get wallet's assets
89
+ server.tool('getAssetsByOwner', 'Get all digital assets (NFTs, cNFTs, tokens) owned by a wallet address. Supports pagination and filtering by collection or token type. Returns asset names, types, and mint addresses.', {
90
+ ownerAddress: z.string().describe('Wallet address'),
91
+ page: z.number().optional().default(1).describe('Page number (starts at 1)'),
92
+ limit: z.number().optional().default(20).describe('Results per page (max 1000)'),
93
+ showFungible: z.boolean().optional().default(false).describe('Include fungible tokens'),
94
+ showNativeBalance: z.boolean().optional().default(false).describe('Include SOL balance')
95
+ }, async ({ ownerAddress, page, limit, showFungible, showNativeBalance }) => {
96
+ try {
97
+ const result = await dasRequest('getAssetsByOwner', {
98
+ ownerAddress,
99
+ page,
100
+ limit,
101
+ displayOptions: {
102
+ showFungible,
103
+ showNativeBalance
104
+ }
105
+ });
106
+ if (!result.items || result.items.length === 0) {
107
+ return {
108
+ content: [{ type: 'text', text: `**Assets for ${formatAddress(ownerAddress)}**\n\nNo assets found.` }]
109
+ };
110
+ }
111
+ const lines = [`**Assets for ${formatAddress(ownerAddress)}** (${result.total} total, page ${page})`, ''];
112
+ result.items.forEach((asset, i) => {
113
+ const name = asset.content?.metadata?.name || asset.token_info?.symbol || 'Unnamed';
114
+ lines.push(`${i + 1}. **${name}** (${asset.interface})`);
115
+ lines.push(` ID: ${formatAddress(asset.id)}`);
116
+ });
117
+ return {
118
+ content: [{ type: 'text', text: lines.join('\n') }]
119
+ };
120
+ }
121
+ catch (err) {
122
+ const errorMsg = err instanceof Error ? err.message : String(err);
123
+ return {
124
+ content: [{ type: 'text', text: `❌ Error: ${errorMsg}` }],
125
+ isError: true
126
+ };
127
+ }
128
+ });
129
+ // getAssetsByGroup - Get collection NFTs
130
+ server.tool('getAssetsByGroup', 'Get all NFTs in a collection by group key/value. Use this for questions like "show me all Mad Lads NFTs" or "how many Claynosaurz are there". The groupKey is usually "collection" and groupValue is the collection mint address.', {
131
+ groupKey: z.string().describe('Group key (usually "collection")'),
132
+ groupValue: z.string().describe('Collection mint address'),
133
+ page: z.number().optional().default(1).describe('Page number'),
134
+ limit: z.number().optional().default(20).describe('Results per page (max 1000)')
135
+ }, async ({ groupKey, groupValue, page, limit }) => {
136
+ try {
137
+ const result = await dasRequest('getAssetsByGroup', {
138
+ groupKey,
139
+ groupValue,
140
+ page,
141
+ limit
142
+ });
143
+ if (!result.items || result.items.length === 0) {
144
+ return {
145
+ content: [{ type: 'text', text: `**Collection ${formatAddress(groupValue)}**\n\nNo assets found.` }]
146
+ };
147
+ }
148
+ const lines = [`**Collection** (${result.total} total, page ${page})`, ''];
149
+ result.items.slice(0, 20).forEach((asset, i) => {
150
+ const name = asset.content?.metadata?.name || 'Unnamed';
151
+ lines.push(`${i + 1}. **${name}**`);
152
+ lines.push(` Owner: ${formatAddress(asset.ownership?.owner || 'Unknown')}`);
153
+ });
154
+ if (result.items.length > 20) {
155
+ lines.push(`\n... and ${result.items.length - 20} more`);
156
+ }
157
+ return {
158
+ content: [{ type: 'text', text: lines.join('\n') }]
159
+ };
160
+ }
161
+ catch (err) {
162
+ const errorMsg = err instanceof Error ? err.message : String(err);
163
+ return {
164
+ content: [{ type: 'text', text: `❌ Error: ${errorMsg}` }],
165
+ isError: true
166
+ };
167
+ }
168
+ });
169
+ // getAssetsByCreator - Get assets by creator address
170
+ server.tool('getAssetsByCreator', 'Find all assets created by a specific creator address. Useful for finding all NFTs or tokens deployed by a particular creator.', {
171
+ creatorAddress: z.string().describe('Creator wallet address'),
172
+ onlyVerified: z.boolean().optional().default(false).describe('Only return verified creators'),
173
+ page: z.number().optional().default(1).describe('Page number'),
174
+ limit: z.number().optional().default(20).describe('Results per page (max 1000)')
175
+ }, async ({ creatorAddress, onlyVerified, page, limit }) => {
176
+ try {
177
+ const result = await dasRequest('getAssetsByCreator', {
178
+ creatorAddress,
179
+ onlyVerified,
180
+ page,
181
+ limit
182
+ });
183
+ if (!result.items || result.items.length === 0) {
184
+ return {
185
+ content: [{ type: 'text', text: `**Assets by ${formatAddress(creatorAddress)}**\n\nNo assets found.` }]
186
+ };
187
+ }
188
+ const lines = [`**Assets by ${formatAddress(creatorAddress)}** (${result.total} total)`, ''];
189
+ result.items.forEach((asset, i) => {
190
+ const name = asset.content?.metadata?.name || asset.token_info?.symbol || 'Unnamed';
191
+ lines.push(`${i + 1}. **${name}** (${asset.interface})`);
192
+ });
193
+ return {
194
+ content: [{ type: 'text', text: lines.join('\n') }]
195
+ };
196
+ }
197
+ catch (err) {
198
+ const errorMsg = err instanceof Error ? err.message : String(err);
199
+ return {
200
+ content: [{ type: 'text', text: `❌ Error: ${errorMsg}` }],
201
+ isError: true
202
+ };
203
+ }
204
+ });
205
+ // getAssetsByAuthority - Get assets by authority
206
+ server.tool('getAssetsByAuthority', 'Get all assets controlled by a specific authority address. Authority addresses have special permissions over assets (update, freeze, etc).', {
207
+ authorityAddress: z.string().describe('Authority wallet address'),
208
+ page: z.number().optional().default(1).describe('Page number'),
209
+ limit: z.number().optional().default(20).describe('Results per page (max 1000)')
210
+ }, async ({ authorityAddress, page, limit }) => {
211
+ try {
212
+ const result = await dasRequest('getAssetsByAuthority', {
213
+ authorityAddress,
214
+ page,
215
+ limit
216
+ });
217
+ if (!result.items || result.items.length === 0) {
218
+ return {
219
+ content: [{ type: 'text', text: `**Assets by authority ${formatAddress(authorityAddress)}**\n\nNo assets found.` }]
220
+ };
221
+ }
222
+ const lines = [`**Assets by authority ${formatAddress(authorityAddress)}** (${result.total} total)`, ''];
223
+ result.items.forEach((asset, i) => {
224
+ const name = asset.content?.metadata?.name || asset.token_info?.symbol || 'Unnamed';
225
+ lines.push(`${i + 1}. **${name}** (${asset.interface})`);
226
+ });
227
+ return {
228
+ content: [{ type: 'text', text: lines.join('\n') }]
229
+ };
230
+ }
231
+ catch (err) {
232
+ const errorMsg = err instanceof Error ? err.message : String(err);
233
+ return {
234
+ content: [{ type: 'text', text: `❌ Error: ${errorMsg}` }],
235
+ isError: true
236
+ };
237
+ }
238
+ });
239
+ // searchAssets - Advanced asset search
240
+ server.tool('searchAssets', 'Advanced search for digital assets with complex filters. Search by name, royalties, burned status, frozen status, and more. Much more powerful than other DAS methods.', {
241
+ query: z.string().optional().describe('Search query string'),
242
+ ownerAddress: z.string().optional().describe('Filter by owner'),
243
+ creatorAddress: z.string().optional().describe('Filter by creator'),
244
+ burnt: z.boolean().optional().describe('Filter burnt assets'),
245
+ frozen: z.boolean().optional().describe('Filter frozen assets'),
246
+ page: z.number().optional().default(1).describe('Page number'),
247
+ limit: z.number().optional().default(20).describe('Results per page (max 1000)')
248
+ }, async (params) => {
249
+ try {
250
+ const result = await dasRequest('searchAssets', params);
251
+ if (!result.items || result.items.length === 0) {
252
+ return {
253
+ content: [{ type: 'text', text: `**Search Results**\n\nNo assets found matching criteria.` }]
254
+ };
255
+ }
256
+ const lines = [`**Search Results** (${result.total} total, page ${params.page})`, ''];
257
+ result.items.forEach((asset, i) => {
258
+ const name = asset.content?.metadata?.name || asset.token_info?.symbol || 'Unnamed';
259
+ lines.push(`${i + 1}. **${name}** (${asset.interface})`);
260
+ });
261
+ return {
262
+ content: [{ type: 'text', text: lines.join('\n') }]
263
+ };
264
+ }
265
+ catch (err) {
266
+ const errorMsg = err instanceof Error ? err.message : String(err);
267
+ return {
268
+ content: [{ type: 'text', text: `❌ Error: ${errorMsg}` }],
269
+ isError: true
270
+ };
271
+ }
272
+ });
273
+ // getAssetProof - Get Merkle proof for compressed NFT
274
+ server.tool('getAssetProof', 'Get the Merkle proof for a compressed NFT (cNFT). Required for transferring or burning compressed NFTs. Returns the proof tree, root, and leaf data.', {
275
+ id: z.string().describe('Compressed NFT mint address')
276
+ }, async ({ id }) => {
277
+ try {
278
+ const proof = await dasRequest('getAssetProof', { id });
279
+ return {
280
+ content: [{
281
+ type: 'text',
282
+ text: `**Merkle Proof for ${formatAddress(id)}**\n\n**Root:** ${formatAddress(proof.root)}\n**Leaf:** ${formatAddress(proof.leaf)}\n**Tree ID:** ${formatAddress(proof.tree_id)}\n**Proof Length:** ${proof.proof.length} nodes`
283
+ }]
284
+ };
285
+ }
286
+ catch (err) {
287
+ const errorMsg = err instanceof Error ? err.message : String(err);
288
+ return {
289
+ content: [{ type: 'text', text: `❌ Error: ${errorMsg}` }],
290
+ isError: true
291
+ };
292
+ }
293
+ });
294
+ // getAssetProofBatch - Batch Merkle proofs
295
+ server.tool('getAssetProofBatch', 'Get Merkle proofs for multiple compressed NFTs in one request (up to 1000). Much faster than calling getAssetProof multiple times.', {
296
+ ids: z.array(z.string()).describe('Array of cNFT mint addresses (up to 1000)')
297
+ }, async ({ ids }) => {
298
+ try {
299
+ if (ids.length > 1000) {
300
+ return {
301
+ content: [{ type: 'text', text: '❌ Maximum 1000 proofs per batch' }],
302
+ isError: true
303
+ };
304
+ }
305
+ const proofs = await dasRequest('getAssetProofBatch', { ids });
306
+ return {
307
+ content: [{
308
+ type: 'text',
309
+ text: `**Batch Merkle Proofs** (${proofs.length} proofs)\n\n${proofs.map((p, i) => `${i + 1}. ${formatAddress(ids[i])}\n Root: ${formatAddress(p.root)}`).join('\n\n')}`
310
+ }]
311
+ };
312
+ }
313
+ catch (err) {
314
+ const errorMsg = err instanceof Error ? err.message : String(err);
315
+ return {
316
+ content: [{ type: 'text', text: `❌ Error: ${errorMsg}` }],
317
+ isError: true
318
+ };
319
+ }
320
+ });
321
+ // getSignaturesForAsset - Get transaction history for compressed NFT
322
+ server.tool('getSignaturesForAsset', 'Get all transaction signatures for a compressed NFT. Shows the complete history of transfers, burns, and other operations on a cNFT.', {
323
+ id: z.string().describe('Compressed NFT mint address'),
324
+ page: z.number().optional().default(1).describe('Page number'),
325
+ limit: z.number().optional().default(20).describe('Results per page (max 1000)')
326
+ }, async ({ id, page, limit }) => {
327
+ try {
328
+ const result = await dasRequest('getSignaturesForAsset', {
329
+ id,
330
+ page,
331
+ limit
332
+ });
333
+ if (!result.items || result.items.length === 0) {
334
+ return {
335
+ content: [{ type: 'text', text: `**Signatures for ${formatAddress(id)}**\n\nNo transactions found.` }]
336
+ };
337
+ }
338
+ const lines = [`**Signatures for ${formatAddress(id)}** (${result.total} total)`, ''];
339
+ result.items.forEach((sig, i) => {
340
+ lines.push(`${i + 1}. ${sig.signature}`);
341
+ if (sig.blockTime) {
342
+ const date = new Date(sig.blockTime * 1000).toLocaleString();
343
+ lines.push(` Time: ${date}`);
344
+ }
345
+ });
346
+ return {
347
+ content: [{ type: 'text', text: lines.join('\n') }]
348
+ };
349
+ }
350
+ catch (err) {
351
+ const errorMsg = err instanceof Error ? err.message : String(err);
352
+ return {
353
+ content: [{ type: 'text', text: `❌ Error: ${errorMsg}` }],
354
+ isError: true
355
+ };
356
+ }
357
+ });
358
+ // getNftEditions - Get edition NFTs for master
359
+ server.tool('getNftEditions', 'Get all edition NFTs for a master NFT. Master NFTs can have multiple editions (copies) with specific edition numbers.', {
360
+ mint: z.string().describe('Master NFT mint address'),
361
+ page: z.number().optional().default(1).describe('Page number'),
362
+ limit: z.number().optional().default(20).describe('Results per page (max 1000)')
363
+ }, async ({ mint, page, limit }) => {
364
+ try {
365
+ const result = await dasRequest('getNftEditions', {
366
+ mint,
367
+ page,
368
+ limit
369
+ });
370
+ if (!result.editions || result.editions.length === 0) {
371
+ return {
372
+ content: [{ type: 'text', text: `**Editions for ${formatAddress(mint)}**\n\nNo editions found.` }]
373
+ };
374
+ }
375
+ const lines = [`**Editions for ${formatAddress(mint)}** (${result.total} total)`, ''];
376
+ result.editions.forEach((edition, i) => {
377
+ lines.push(`${i + 1}. Edition #${edition.edition_number}`);
378
+ lines.push(` Mint: ${formatAddress(edition.mint)}`);
379
+ if (edition.owner) {
380
+ lines.push(` Owner: ${formatAddress(edition.owner)}`);
381
+ }
382
+ });
383
+ return {
384
+ content: [{ type: 'text', text: lines.join('\n') }]
385
+ };
386
+ }
387
+ catch (err) {
388
+ const errorMsg = err instanceof Error ? err.message : String(err);
389
+ return {
390
+ content: [{ type: 'text', text: `❌ Error: ${errorMsg}` }],
391
+ isError: true
392
+ };
393
+ }
394
+ });
395
+ // getTokenAccounts - Query token accounts
396
+ server.tool('getTokenAccounts', 'Query token accounts with advanced filters. Can filter by mint, owner, amount, delegate, frozen status, and more.', {
397
+ mint: z.string().optional().describe('Filter by mint address'),
398
+ owner: z.string().optional().describe('Filter by owner address'),
399
+ page: z.number().optional().default(1).describe('Page number'),
400
+ limit: z.number().optional().default(20).describe('Results per page (max 1000)')
401
+ }, async (params) => {
402
+ try {
403
+ const result = await dasRequest('getTokenAccounts', params);
404
+ if (!result.token_accounts || result.token_accounts.length === 0) {
405
+ return {
406
+ content: [{ type: 'text', text: `**Token Accounts**\n\nNo accounts found matching criteria.` }]
407
+ };
408
+ }
409
+ const lines = [`**Token Accounts** (${result.total} total)`, ''];
410
+ result.token_accounts.forEach((account, i) => {
411
+ lines.push(`${i + 1}. ${formatAddress(account.address)}`);
412
+ lines.push(` Owner: ${formatAddress(account.owner)}`);
413
+ lines.push(` Mint: ${formatAddress(account.mint)}`);
414
+ lines.push(` Balance: ${account.amount}`);
415
+ });
416
+ return {
417
+ content: [{ type: 'text', text: lines.join('\n') }]
418
+ };
419
+ }
420
+ catch (err) {
421
+ const errorMsg = err instanceof Error ? err.message : String(err);
422
+ return {
423
+ content: [{ type: 'text', text: `❌ Error: ${errorMsg}` }],
424
+ isError: true
425
+ };
426
+ }
427
+ });
428
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerEnhancedTransactionTools(server: McpServer): void;