helius-mcp 0.5.3 → 1.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 (67) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/LICENSE +1 -1
  3. package/README.md +97 -21
  4. package/dist/http.d.ts +1 -0
  5. package/dist/http.js +2 -0
  6. package/dist/index.js +93 -2
  7. package/dist/scripts/validate-catalog.d.ts +13 -0
  8. package/dist/scripts/validate-catalog.js +76 -0
  9. package/dist/tools/accounts.js +114 -204
  10. package/dist/tools/assets.js +109 -123
  11. package/dist/tools/auth.d.ts +2 -0
  12. package/dist/tools/auth.js +459 -0
  13. package/dist/tools/balance.js +28 -32
  14. package/dist/tools/blocks.js +68 -87
  15. package/dist/tools/config.js +18 -79
  16. package/dist/tools/das-extras.js +56 -41
  17. package/dist/tools/docs.js +12 -54
  18. package/dist/tools/enhanced-websockets.js +104 -74
  19. package/dist/tools/fees.js +42 -61
  20. package/dist/tools/guides.js +126 -515
  21. package/dist/tools/index.js +50 -2
  22. package/dist/tools/laserstream.js +107 -53
  23. package/dist/tools/network.js +47 -69
  24. package/dist/tools/plans.d.ts +21 -0
  25. package/dist/tools/plans.js +105 -246
  26. package/dist/tools/product-catalog.d.ts +10 -0
  27. package/dist/tools/product-catalog.js +123 -0
  28. package/dist/tools/recommend.d.ts +4 -0
  29. package/dist/tools/recommend.js +233 -0
  30. package/dist/tools/shared.js +8 -3
  31. package/dist/tools/solana-knowledge.d.ts +2 -0
  32. package/dist/tools/solana-knowledge.js +544 -0
  33. package/dist/tools/tokens.js +17 -18
  34. package/dist/tools/transactions.js +232 -302
  35. package/dist/tools/transfers.d.ts +2 -0
  36. package/dist/tools/transfers.js +270 -0
  37. package/dist/tools/wallet.js +175 -177
  38. package/dist/tools/webhooks.js +80 -82
  39. package/dist/types/transaction-types.d.ts +1 -1
  40. package/dist/types/transaction-types.js +2 -1
  41. package/dist/utils/config.d.ts +27 -0
  42. package/dist/utils/config.js +76 -0
  43. package/dist/utils/docs.d.ts +24 -0
  44. package/dist/utils/docs.js +72 -0
  45. package/dist/utils/errors.d.ts +32 -0
  46. package/dist/utils/errors.js +157 -0
  47. package/dist/utils/feedback.d.ts +16 -0
  48. package/dist/utils/feedback.js +87 -0
  49. package/dist/utils/formatters.d.ts +0 -1
  50. package/dist/utils/formatters.js +0 -3
  51. package/dist/utils/helius.d.ts +15 -5
  52. package/dist/utils/helius.js +52 -45
  53. package/dist/version.d.ts +1 -0
  54. package/dist/version.js +1 -0
  55. package/package.json +17 -7
  56. package/system-prompts/helius/claude.system.md +170 -0
  57. package/system-prompts/helius/full.md +2868 -0
  58. package/system-prompts/helius/openai.developer.md +170 -0
  59. package/system-prompts/helius-dflow/claude.system.md +290 -0
  60. package/system-prompts/helius-dflow/full.md +3647 -0
  61. package/system-prompts/helius-dflow/openai.developer.md +290 -0
  62. package/system-prompts/helius-phantom/claude.system.md +348 -0
  63. package/system-prompts/helius-phantom/full.md +5472 -0
  64. package/system-prompts/helius-phantom/openai.developer.md +348 -0
  65. package/system-prompts/svm/claude.system.md +174 -0
  66. package/system-prompts/svm/full.md +699 -0
  67. package/system-prompts/svm/openai.developer.md +174 -0
@@ -1,7 +1,8 @@
1
1
  import { z } from 'zod';
2
- import { getHeliusClient, hasApiKey, getRpcUrl } from '../utils/helius.js';
2
+ import { getHeliusClient, hasApiKey } from '../utils/helius.js';
3
3
  import { formatAddress, formatSol } from '../utils/formatters.js';
4
4
  import { noApiKeyResponse } from './shared.js';
5
+ import { mcpText, validateEnum, handleToolError, addressError, paginationError } from '../utils/errors.js';
5
6
  function formatParsedAccountData(account) {
6
7
  const lines = [];
7
8
  const parsedData = account.data;
@@ -20,145 +21,88 @@ function formatParsedAccountData(account) {
20
21
  return lines;
21
22
  }
22
23
  export function registerAccountTools(server) {
23
- // Get Account Info (single or batch) — uses direct JSON-RPC
24
- server.tool('getAccountInfo', 'Get detailed Solana account information for one or more accounts. For a single account: returns owner program, lamport balance, data size, executable status, and rent epoch. For batch: pass up to 100 addresses in "addresses" for fast bulk lookups. Use jsonParsed encoding (default) on token mint addresses to see Token-2022 extensions, authorities, and supply data. Use this to inspect any on-chain account.', {
24
+ // Get Account Info (single or batch) — uses SDK standard Solana RPC (Kit, bigint)
25
+ server.tool('getAccountInfo', 'BEST FOR: raw on-chain account inspection (owner program, data size, executable status, Token-2022 extensions). PREFER getAsset for token/NFT metadata. PREFER getBalance for SOL balance. Get detailed Solana account information for one or more accounts. For a single account: returns owner program, lamport balance, data size, executable status, and rent epoch. For batch: pass up to 100 addresses in "addresses" for fast bulk lookups. Use jsonParsed encoding (default) on token mint addresses to see Token-2022 extensions, authorities, and supply data. Use this to inspect any on-chain account. Credit cost: 1 credit (standard RPC).', {
25
26
  address: z.string().optional().describe('Single account address (base58 encoded). Use this OR addresses, not both.'),
26
- addresses: z.array(z.string()).optional().describe('Array of account addresses for batch lookup (up to 100). Use this OR address, not both.'),
27
- encoding: z.enum(['base58', 'base64', 'jsonParsed']).optional().default('jsonParsed').describe('Data encoding format')
27
+ addresses: z.array(z.string()).optional().describe('Array of account addresses for batch lookup (base58 encoded, up to 100). Use this OR address, not both.'),
28
+ encoding: z.string().optional().default('jsonParsed').describe('Data encoding format')
28
29
  }, async ({ address, addresses, encoding }) => {
29
30
  if (!hasApiKey())
30
31
  return noApiKeyResponse();
31
- const url = getRpcUrl();
32
+ const err = validateEnum(encoding, ['base58', 'base64', 'jsonParsed'], 'Account Info Error', 'encoding');
33
+ if (err)
34
+ return err;
32
35
  // Validate: must provide exactly one of address or addresses
33
36
  if (!address && (!addresses || addresses.length === 0)) {
34
- return {
35
- content: [{
36
- type: 'text',
37
- text: `**Error:** Provide either \`address\` (single account) or \`addresses\` (batch of up to 100).`
38
- }]
39
- };
37
+ return mcpText(`**Error:** Provide either \`address\` (single account) or \`addresses\` (batch of up to 100).`);
40
38
  }
41
39
  if (address && addresses && addresses.length > 0) {
42
- return {
43
- content: [{
44
- type: 'text',
45
- text: `**Error:** Provide either \`address\` or \`addresses\`, not both.`
46
- }]
47
- };
40
+ return mcpText(`**Error:** Provide either \`address\` or \`addresses\`, not both.`);
48
41
  }
49
- // --- Batch mode ---
50
- if (addresses && addresses.length > 0) {
51
- if (addresses.length > 100) {
52
- return {
53
- content: [{
54
- type: 'text',
55
- text: `**Error:** Maximum 100 accounts per request. You provided ${addresses.length}.`
56
- }]
57
- };
58
- }
59
- const response = await fetch(url, {
60
- method: 'POST',
61
- headers: { 'Content-Type': 'application/json' },
62
- body: JSON.stringify({
63
- jsonrpc: '2.0',
64
- id: 'get-multiple-accounts',
65
- method: 'getMultipleAccounts',
66
- params: [addresses, { encoding }]
67
- })
68
- });
69
- const data = await response.json();
70
- if (data.error) {
71
- return {
72
- content: [{
73
- type: 'text',
74
- text: `**Error**\n\n${data.error.message}`
75
- }]
76
- };
77
- }
78
- const accounts = data.result?.value || [];
79
- const lines = [`**Multiple Accounts** (${addresses.length} requested)`, ''];
80
- addresses.forEach((addr, i) => {
81
- const account = accounts[i];
82
- if (!account) {
83
- lines.push(`**${formatAddress(addr)}:** Not found`);
42
+ try {
43
+ const helius = getHeliusClient();
44
+ // --- Batch mode ---
45
+ if (addresses && addresses.length > 0) {
46
+ if (addresses.length > 100) {
47
+ return mcpText(`**Error:** Maximum 100 accounts per request. You provided ${addresses.length}.`);
84
48
  }
85
- else {
86
- lines.push(`**${formatAddress(addr)}**`);
87
- lines.push(` Balance: ${formatSol(account.lamports)}`);
88
- lines.push(` Owner: ${account.owner}`);
89
- lines.push(` Executable: ${account.executable ? 'Yes' : 'No'}`);
90
- if (account.space !== undefined) {
91
- lines.push(` Data Size: ${account.space} bytes`);
49
+ const result = await helius.getMultipleAccounts(addresses, { encoding });
50
+ const accounts = result?.value || [];
51
+ const lines = [`**Multiple Accounts** (${addresses.length} requested)`, ''];
52
+ addresses.forEach((addr, i) => {
53
+ const account = accounts[i];
54
+ if (!account) {
55
+ lines.push(`**${formatAddress(addr)}:** Not found`);
92
56
  }
93
- // Show parsed data (Token-2022 extensions, mint info, etc.)
94
- const parsedLines = formatParsedAccountData(account);
95
- lines.push(...parsedLines);
96
- }
97
- lines.push('');
98
- });
99
- return {
100
- content: [{
101
- type: 'text',
102
- text: lines.join('\n')
103
- }]
104
- };
105
- }
106
- // --- Single account mode ---
107
- const response = await fetch(url, {
108
- method: 'POST',
109
- headers: { 'Content-Type': 'application/json' },
110
- body: JSON.stringify({
111
- jsonrpc: '2.0',
112
- id: 'get-account-info',
113
- method: 'getAccountInfo',
114
- params: [address, { encoding }]
115
- })
116
- });
117
- const data = await response.json();
118
- if (data.error) {
119
- return {
120
- content: [{
121
- type: 'text',
122
- text: `**Error**\n\n${data.error.message}`
123
- }]
124
- };
125
- }
126
- const account = data.result?.value;
127
- if (!account) {
128
- return {
129
- content: [{
130
- type: 'text',
131
- text: `**Account ${formatAddress(address)}**\n\nAccount not found or has no data.`
132
- }]
133
- };
134
- }
135
- const lines = [
136
- `**Account ${formatAddress(address)}**`,
137
- '',
138
- `**Balance:** ${formatSol(account.lamports)} (${account.lamports.toLocaleString()} lamports)`,
139
- `**Owner:** ${account.owner}`,
140
- `**Executable:** ${account.executable ? 'Yes' : 'No'}`,
141
- ];
142
- if (account.space !== undefined) {
143
- lines.push(`**Data Size:** ${account.space} bytes`);
57
+ else {
58
+ lines.push(`**${formatAddress(addr)}**`);
59
+ lines.push(` Balance: ${formatSol(Number(account.lamports))}`);
60
+ lines.push(` Owner: ${account.owner}`);
61
+ lines.push(` Executable: ${account.executable ? 'Yes' : 'No'}`);
62
+ if (account.space !== undefined) {
63
+ lines.push(` Data Size: ${Number(account.space)} bytes`);
64
+ }
65
+ const parsedLines = formatParsedAccountData({ ...account, lamports: Number(account.lamports) });
66
+ lines.push(...parsedLines);
67
+ }
68
+ lines.push('');
69
+ });
70
+ return mcpText(lines.join('\n'));
71
+ }
72
+ // --- Single account mode ---
73
+ const result = await helius.getAccountInfo(address, { encoding });
74
+ const account = result?.value;
75
+ if (!account) {
76
+ return mcpText(`**Account ${formatAddress(address)}**\n\nAccount not found or has no data.`);
77
+ }
78
+ const lamports = Number(account.lamports);
79
+ const lines = [
80
+ `**Account ${formatAddress(address)}**`,
81
+ '',
82
+ `**Balance:** ${formatSol(lamports)} (${lamports.toLocaleString()} lamports)`,
83
+ `**Owner:** ${account.owner}`,
84
+ `**Executable:** ${account.executable ? 'Yes' : 'No'}`,
85
+ ];
86
+ if (account.space !== undefined) {
87
+ lines.push(`**Data Size:** ${Number(account.space)} bytes`);
88
+ }
89
+ const parsedLines = formatParsedAccountData({ ...account, lamports });
90
+ if (parsedLines.length > 0) {
91
+ lines.push('', '**Parsed Data:**');
92
+ lines.push(...parsedLines);
93
+ }
94
+ return mcpText(lines.join('\n'));
144
95
  }
145
- // Show parsed data details when using jsonParsed
146
- const parsedLines = formatParsedAccountData(account);
147
- if (parsedLines.length > 0) {
148
- lines.push('', '**Parsed Data:**');
149
- lines.push(...parsedLines);
96
+ catch (err) {
97
+ return handleToolError(err, 'Error fetching account info', [
98
+ addressError('Account Info'),
99
+ ]);
150
100
  }
151
- return {
152
- content: [{
153
- type: 'text',
154
- text: lines.join('\n')
155
- }]
156
- };
157
101
  });
158
102
  // Get Token Accounts
159
- server.tool('getTokenAccounts', 'Query token accounts with advanced filters. Can filter by mint address, owner address, or both. Returns token account addresses and balances.', {
160
- owner: z.string().optional().describe('Filter by owner wallet address'),
161
- mint: z.string().optional().describe('Filter by token mint address'),
103
+ server.tool('getTokenAccounts', 'BEST FOR: advanced token account queries with flexible filters (by mint, owner, or both). PREFER getTokenHolders for a quick top-holders list. PREFER getTokenBalances for a wallet\'s token holdings with prices. Query token accounts with advanced filters. Can filter by mint address, owner address, or both. Returns token account addresses and balances. Credit cost: 10 credits/call (DAS API).', {
104
+ owner: z.string().optional().describe('Filter by owner wallet address (base58 encoded)'),
105
+ mint: z.string().optional().describe('Filter by token mint address (base58 encoded)'),
162
106
  page: z.number().optional().default(1).describe('Page number (starts at 1)'),
163
107
  limit: z.number().optional().default(20).describe('Results per page (max 1000)')
164
108
  }, async ({ owner, mint, page, limit }) => {
@@ -166,19 +110,23 @@ export function registerAccountTools(server) {
166
110
  return noApiKeyResponse();
167
111
  const helius = getHeliusClient();
168
112
  if (!owner && !mint) {
169
- return {
170
- content: [{
171
- type: 'text',
172
- text: `**Error:** You must provide at least one of: owner or mint address.`
173
- }]
174
- };
113
+ return mcpText(`**Error:** You must provide at least one of: owner or mint address.`);
175
114
  }
176
115
  const params = { page, limit };
177
116
  if (owner)
178
117
  params.owner = owner;
179
118
  if (mint)
180
119
  params.mint = mint;
181
- const response = await helius.getTokenAccounts(params);
120
+ let response;
121
+ try {
122
+ response = await helius.getTokenAccounts(params);
123
+ }
124
+ catch (err) {
125
+ return handleToolError(err, 'Error fetching token accounts', [
126
+ addressError('Token Accounts', 'Invalid Solana address. Please provide valid base58-encoded addresses for owner and/or mint.'),
127
+ paginationError('Token Accounts'),
128
+ ]);
129
+ }
182
130
  const items = (response.token_accounts || []);
183
131
  if (items.length === 0) {
184
132
  const filterDesc = owner && mint
@@ -186,12 +134,7 @@ export function registerAccountTools(server) {
186
134
  : owner
187
135
  ? `owner=${formatAddress(owner)}`
188
136
  : `mint=${formatAddress(mint)}`;
189
- return {
190
- content: [{
191
- type: 'text',
192
- text: `**Token Accounts** (${filterDesc})\n\nNo token accounts found.`
193
- }]
194
- };
137
+ return mcpText(`**Token Accounts** (${filterDesc})\n\nNo token accounts found.`);
195
138
  }
196
139
  const lines = [`**Token Accounts** (${response.total || items.length} total, page ${page})`, ''];
197
140
  items.forEach((account) => {
@@ -210,24 +153,22 @@ export function registerAccountTools(server) {
210
153
  }
211
154
  lines.push('');
212
155
  });
213
- return {
214
- content: [{
215
- type: 'text',
216
- text: lines.join('\n')
217
- }]
218
- };
156
+ return mcpText(lines.join('\n'));
219
157
  });
220
- // Get Program Accounts (V2 with pagination)
221
- server.tool('getProgramAccounts', 'Get all accounts owned by a specific program. Returns account addresses, balances, and data sizes. Use dataSize to filter by account data length (e.g. 165 for token accounts). Useful for finding all accounts created by a program like a DEX, lending protocol, or custom program.', {
158
+ // Get Program Accounts (V2 with pagination) — uses SDK RpcCaller (no bigint)
159
+ server.tool('getProgramAccounts', 'BEST FOR: investigating protocol state — finding DEX pools, lending positions, or all accounts created by a specific program. PREFER searchAssets for NFT/token asset searches by creator or authority. Get all accounts owned by a specific program. Returns account addresses, balances, and data sizes. Use dataSize to filter by account data length (e.g. 165 for token accounts). Useful for finding all accounts created by a program like a DEX, lending protocol, or custom program. Credit cost: 10 credits/call.', {
222
160
  programId: z.string().describe('Program ID (base58 encoded) — the owner program of the accounts to find'),
223
161
  limit: z.number().optional().default(20).describe('Maximum accounts to return (default 20, max 100)'),
224
- encoding: z.enum(['base58', 'base64', 'jsonParsed']).optional().default('base64').describe('Data encoding format'),
162
+ encoding: z.string().optional().default('base64').describe('Data encoding format'),
225
163
  dataSize: z.number().optional().describe('Filter by exact account data size in bytes (e.g. 165 for SPL token accounts)'),
226
164
  paginationKey: z.string().optional().describe('Pagination cursor from a previous response to fetch the next page')
227
165
  }, async ({ programId, limit, encoding, dataSize, paginationKey }) => {
228
166
  if (!hasApiKey())
229
167
  return noApiKeyResponse();
230
- const url = getRpcUrl();
168
+ const encErr = validateEnum(encoding, ['base58', 'base64', 'jsonParsed'], 'Program Accounts Error', 'encoding');
169
+ if (encErr)
170
+ return encErr;
171
+ const helius = getHeliusClient();
231
172
  const cappedLimit = Math.min(limit, 10_000);
232
173
  const filters = [];
233
174
  if (dataSize !== undefined) {
@@ -242,66 +183,35 @@ export function registerAccountTools(server) {
242
183
  rpcParams.filters = filters;
243
184
  if (paginationKey)
244
185
  rpcParams.paginationKey = paginationKey;
245
- const requestBody = {
246
- jsonrpc: '2.0',
247
- id: 'get-program-accounts-v2',
248
- method: 'getProgramAccountsV2',
249
- params: [programId, rpcParams]
250
- };
251
- let data;
252
186
  try {
253
- const response = await fetch(url, {
254
- method: 'POST',
255
- headers: { 'Content-Type': 'application/json' },
256
- body: JSON.stringify(requestBody)
187
+ const data = await helius.getProgramAccountsV2([programId, rpcParams]);
188
+ // SDK returns result directly (or may wrap in RpcResponse with context/value)
189
+ const result = data.value ?? data;
190
+ const accounts = result?.accounts || [];
191
+ if (accounts.length === 0) {
192
+ return mcpText(`**Program Accounts for ${formatAddress(programId)}**\n\nNo accounts found.`);
193
+ }
194
+ const totalLabel = result?.totalResults
195
+ ? `${result.totalResults.toLocaleString()} total`
196
+ : `${accounts.length} returned`;
197
+ const lines = [`**Program Accounts for ${formatAddress(programId)}** (${totalLabel})`, ''];
198
+ accounts.forEach((item) => {
199
+ lines.push(`- **${formatAddress(item.pubkey)}**`);
200
+ lines.push(` Balance: ${formatSol(item.account.lamports)}`);
201
+ if (item.account.space !== undefined) {
202
+ lines.push(` Data Size: ${item.account.space} bytes`);
203
+ }
204
+ lines.push(` Executable: ${item.account.executable ? 'Yes' : 'No'}`);
257
205
  });
258
- data = await response.json();
206
+ if (result?.paginationKey) {
207
+ lines.push('', `**Next Page:** Pass \`paginationKey: "${result.paginationKey}"\` to fetch the next page.`);
208
+ }
209
+ return mcpText(lines.join('\n'));
259
210
  }
260
211
  catch (err) {
261
- return {
262
- content: [{
263
- type: 'text',
264
- text: `**Error fetching program accounts:** ${err instanceof Error ? err.message : String(err)}`
265
- }]
266
- };
267
- }
268
- if (data.error) {
269
- return {
270
- content: [{
271
- type: 'text',
272
- text: `**Error**\n\n${data.error.message}`
273
- }]
274
- };
275
- }
276
- const accounts = data.result?.accounts || [];
277
- if (accounts.length === 0) {
278
- return {
279
- content: [{
280
- type: 'text',
281
- text: `**Program Accounts for ${formatAddress(programId)}**\n\nNo accounts found.`
282
- }]
283
- };
212
+ return handleToolError(err, 'Error fetching program accounts', [
213
+ addressError('Program Accounts'),
214
+ ]);
284
215
  }
285
- const totalLabel = data.result?.totalResults
286
- ? `${data.result.totalResults.toLocaleString()} total`
287
- : `${accounts.length} returned`;
288
- const lines = [`**Program Accounts for ${formatAddress(programId)}** (${totalLabel})`, ''];
289
- accounts.forEach((item) => {
290
- lines.push(`- **${formatAddress(item.pubkey)}**`);
291
- lines.push(` Balance: ${formatSol(item.account.lamports)}`);
292
- if (item.account.space !== undefined) {
293
- lines.push(` Data Size: ${item.account.space} bytes`);
294
- }
295
- lines.push(` Executable: ${item.account.executable ? 'Yes' : 'No'}`);
296
- });
297
- if (data.result?.paginationKey) {
298
- lines.push('', `**Next Page:** Pass \`paginationKey: "${data.result.paginationKey}"\` to fetch the next page.`);
299
- }
300
- return {
301
- content: [{
302
- type: 'text',
303
- text: lines.join('\n')
304
- }]
305
- };
306
216
  });
307
217
  }