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.
- package/LICENSE +21 -0
- package/README.md +150 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +29 -0
- package/dist/tools/accounts.d.ts +2 -0
- package/dist/tools/accounts.js +197 -0
- package/dist/tools/assets-extended.d.ts +2 -0
- package/dist/tools/assets-extended.js +54 -0
- package/dist/tools/assets.d.ts +2 -0
- package/dist/tools/assets.js +156 -0
- package/dist/tools/balance.d.ts +2 -0
- package/dist/tools/balance.js +169 -0
- package/dist/tools/config.d.ts +2 -0
- package/dist/tools/config.js +33 -0
- package/dist/tools/das.d.ts +2 -0
- package/dist/tools/das.js +428 -0
- package/dist/tools/enhanced-transactions.d.ts +2 -0
- package/dist/tools/enhanced-transactions.js +51 -0
- package/dist/tools/enhanced-websockets.d.ts +12 -0
- package/dist/tools/enhanced-websockets.js +329 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +18 -0
- package/dist/tools/laserstream.d.ts +16 -0
- package/dist/tools/laserstream.js +348 -0
- package/dist/tools/priority-fees.d.ts +2 -0
- package/dist/tools/priority-fees.js +61 -0
- package/dist/tools/rpc.d.ts +2 -0
- package/dist/tools/rpc.js +130 -0
- package/dist/tools/shared.d.ts +6 -0
- package/dist/tools/shared.js +13 -0
- package/dist/tools/transactions-enhanced.d.ts +2 -0
- package/dist/tools/transactions-enhanced.js +165 -0
- package/dist/tools/transactions.d.ts +2 -0
- package/dist/tools/transactions.js +336 -0
- package/dist/tools/webhooks.d.ts +2 -0
- package/dist/tools/webhooks.js +156 -0
- package/dist/utils/formatters.d.ts +4 -0
- package/dist/utils/formatters.js +12 -0
- package/dist/utils/helius.d.ts +33 -0
- package/dist/utils/helius.js +134 -0
- 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,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,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
|
+
}
|