helius-mcp 1.2.0 → 2.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.
Files changed (94) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +42 -30
  3. package/dist/http.d.ts +1 -1
  4. package/dist/index.js +2 -56
  5. package/dist/results/store.d.ts +8 -0
  6. package/dist/results/store.js +72 -0
  7. package/dist/results/types.d.ts +47 -0
  8. package/dist/results/types.js +1 -0
  9. package/dist/router/action-groups.d.ts +6 -0
  10. package/dist/router/action-groups.js +32 -0
  11. package/dist/router/action-handlers.d.ts +20 -0
  12. package/dist/router/action-handlers.js +125 -0
  13. package/dist/router/actions.d.ts +12 -0
  14. package/dist/router/actions.js +123 -0
  15. package/dist/router/catalog.d.ts +6 -0
  16. package/dist/router/catalog.js +388 -0
  17. package/dist/router/context.d.ts +5 -0
  18. package/dist/router/context.js +10 -0
  19. package/dist/router/dispatch.d.ts +4 -0
  20. package/dist/router/dispatch.js +276 -0
  21. package/dist/router/instructions.d.ts +1 -0
  22. package/dist/router/instructions.js +25 -0
  23. package/dist/router/register.d.ts +2 -0
  24. package/dist/router/register.js +15 -0
  25. package/dist/router/required-params.d.ts +9 -0
  26. package/dist/router/required-params.js +66 -0
  27. package/dist/router/responses.d.ts +29 -0
  28. package/dist/router/responses.js +186 -0
  29. package/dist/router/schemas.d.ts +216 -0
  30. package/dist/router/schemas.js +195 -0
  31. package/dist/router/telemetry.d.ts +27 -0
  32. package/dist/router/telemetry.js +52 -0
  33. package/dist/router/types.d.ts +46 -0
  34. package/dist/router/types.js +1 -0
  35. package/dist/scripts/validate-catalog.d.ts +2 -2
  36. package/dist/scripts/validate-catalog.js +10 -10
  37. package/dist/tools/accounts.js +5 -5
  38. package/dist/tools/assets.js +5 -5
  39. package/dist/tools/auth.js +392 -288
  40. package/dist/tools/config.js +3 -3
  41. package/dist/tools/das-extras.js +6 -6
  42. package/dist/tools/docs.js +55 -41
  43. package/dist/tools/enhanced-websockets.js +13 -13
  44. package/dist/tools/fees.js +3 -3
  45. package/dist/tools/index.d.ts +1 -1
  46. package/dist/tools/index.js +2 -80
  47. package/dist/tools/laserstream.js +20 -23
  48. package/dist/tools/network.js +41 -2
  49. package/dist/tools/plans.d.ts +0 -5
  50. package/dist/tools/plans.js +167 -12
  51. package/dist/tools/product-catalog.d.ts +1 -0
  52. package/dist/tools/product-catalog.js +51 -16
  53. package/dist/tools/recommend.d.ts +0 -1
  54. package/dist/tools/recommend.js +9 -28
  55. package/dist/tools/shared.d.ts +1 -0
  56. package/dist/tools/shared.js +10 -2
  57. package/dist/tools/solana-knowledge.js +23 -7
  58. package/dist/tools/staking.d.ts +2 -0
  59. package/dist/tools/staking.js +268 -0
  60. package/dist/tools/transactions.js +167 -3
  61. package/dist/tools/transfers.js +38 -43
  62. package/dist/tools/wallet.js +27 -16
  63. package/dist/tools/webhooks.js +3 -3
  64. package/dist/tools/zk-compression.d.ts +2 -0
  65. package/dist/tools/zk-compression.js +781 -0
  66. package/dist/utils/config.d.ts +2 -2
  67. package/dist/utils/config.js +68 -6
  68. package/dist/utils/errors.d.ts +10 -1
  69. package/dist/utils/errors.js +46 -12
  70. package/dist/utils/feedback.js +1 -4
  71. package/dist/utils/helius.js +2 -1
  72. package/dist/utils/ows.d.ts +74 -0
  73. package/dist/utils/ows.js +155 -0
  74. package/dist/version.d.ts +1 -1
  75. package/dist/version.js +1 -1
  76. package/package.json +2 -2
  77. package/system-prompts/helius/claude.system.md +56 -25
  78. package/system-prompts/helius/full.md +474 -130
  79. package/system-prompts/helius/openai.developer.md +56 -25
  80. package/system-prompts/helius-dflow/claude.system.md +41 -6
  81. package/system-prompts/helius-dflow/full.md +581 -92
  82. package/system-prompts/helius-dflow/openai.developer.md +41 -6
  83. package/system-prompts/helius-jupiter/claude.system.md +333 -0
  84. package/system-prompts/helius-jupiter/full.md +5109 -0
  85. package/system-prompts/helius-jupiter/openai.developer.md +333 -0
  86. package/system-prompts/helius-okx/claude.system.md +182 -0
  87. package/system-prompts/helius-okx/full.md +584 -0
  88. package/system-prompts/helius-okx/openai.developer.md +182 -0
  89. package/system-prompts/helius-phantom/claude.system.md +15 -2
  90. package/system-prompts/helius-phantom/full.md +254 -101
  91. package/system-prompts/helius-phantom/openai.developer.md +15 -2
  92. package/system-prompts/svm/claude.system.md +1 -0
  93. package/system-prompts/svm/full.md +1 -0
  94. package/system-prompts/svm/openai.developer.md +1 -0
@@ -0,0 +1,276 @@
1
+ import { getStoredResult, putStoredResult } from '../results/store.js';
2
+ import { callActionHandler } from './action-handlers.js';
3
+ import { getActionCatalogEntry } from './catalog.js';
4
+ import { getRouterContext } from './context.js';
5
+ import { ACTION_REQUIRED_PARAMS } from './required-params.js';
6
+ import { applyItemSelection, applyRangeSelection, applySectionSelection, buildTextVariant, collectSectionHints, compactErrorText, estimateRenderedSize, getFamilyLimit, mcpErrorCompact, mcpResultHandle, mcpText, toPlainText, } from './responses.js';
7
+ function isHandleEligible(entry) {
8
+ return entry.handleEligibility;
9
+ }
10
+ function pickRequestedDetail(entry, detail) {
11
+ if (detail === 'summary' || detail === 'standard' || detail === 'full') {
12
+ return detail;
13
+ }
14
+ return entry.defaultDetail;
15
+ }
16
+ function buildActionParams(input) {
17
+ const { action: _action, detail: _detail, args, ...topLevel } = input;
18
+ const merged = {
19
+ ...(args && typeof args === 'object' && !Array.isArray(args) ? args : {}),
20
+ };
21
+ for (const [key, value] of Object.entries(topLevel)) {
22
+ if (value !== undefined) {
23
+ merged[key] = value;
24
+ }
25
+ }
26
+ // Normalize common address field mismatches
27
+ if (!merged.address && merged.ownerAddress)
28
+ merged.address = merged.ownerAddress;
29
+ if (!merged.owner && merged.address && !merged.ownerAddress)
30
+ merged.owner = merged.address;
31
+ if (!merged.address && merged.owner)
32
+ merged.address = merged.owner;
33
+ return merged;
34
+ }
35
+ function detectContinuation(action, params, text) {
36
+ if (action === 'getTransactionHistory') {
37
+ // Fragile: depends on handler markdown format "**Next Page Token:** `<token>`"
38
+ const tokenMatch = text.match(/\*\*Next Page Token:\*\*\s+`([^`]+)`/);
39
+ if (tokenMatch) {
40
+ const mode = typeof params.mode === 'string' ? params.mode : 'parsed';
41
+ const next = mode === 'raw'
42
+ ? { kind: 'rawApi', paginationToken: tokenMatch[1] }
43
+ : { kind: 'historyApi', paginationToken: tokenMatch[1] };
44
+ return { model: 'transactionHistory', next };
45
+ }
46
+ const hasFilters = [
47
+ 'paginationToken',
48
+ 'status',
49
+ 'tokenAccounts',
50
+ 'blockTimeGte',
51
+ 'blockTimeLte',
52
+ 'slotGte',
53
+ 'slotLte',
54
+ ].some((key) => params[key] !== undefined);
55
+ const mode = typeof params.mode === 'string' ? params.mode : 'parsed';
56
+ const sortOrder = typeof params.sortOrder === 'string' ? params.sortOrder : 'desc';
57
+ if (mode === 'signatures' && sortOrder === 'desc' && !hasFilters) {
58
+ // Fragile: depends on handler "✅/❌ <base58sig>" line format for signature extraction
59
+ const signatures = Array.from(text.matchAll(/^[^\S\r\n]*[✅❌]\s+([1-9A-HJ-NP-Za-km-z]{86,88})$/gm)).map((match) => match[1]);
60
+ const lastSignature = signatures.at(-1);
61
+ if (lastSignature) {
62
+ return {
63
+ model: 'transactionHistory',
64
+ next: {
65
+ kind: 'signaturesQuick',
66
+ nextBefore: lastSignature,
67
+ lastSeenSignature: lastSignature,
68
+ until: typeof params.until === 'string' ? params.until : undefined,
69
+ },
70
+ };
71
+ }
72
+ }
73
+ }
74
+ if (action === 'getTransfersByAddress') {
75
+ // Fragile: depends on handler markdown format "**Next Page Token:** `<token>`" (mirrors getTransactionHistory)
76
+ const tokenMatch = text.match(/\*\*Next Page Token:\*\*\s+`([^`]+)`/);
77
+ if (tokenMatch) {
78
+ return {
79
+ model: 'transactionHistory',
80
+ next: { kind: 'rawApi', paginationToken: tokenMatch[1] },
81
+ };
82
+ }
83
+ }
84
+ if (typeof params.page === 'number') {
85
+ return { model: 'page', nextPage: params.page + 1 };
86
+ }
87
+ return { model: 'none' };
88
+ }
89
+ function buildAvailableExpansions(family, continuation, sectionHints) {
90
+ const expansions = new Set(['full']);
91
+ if (family === 'document' || sectionHints.length > 0) {
92
+ expansions.add('section');
93
+ expansions.add('range');
94
+ }
95
+ if (family === 'list' || family === 'history') {
96
+ expansions.add('item');
97
+ }
98
+ if (continuation.model === 'page') {
99
+ expansions.add('page');
100
+ }
101
+ if (continuation.model === 'transactionHistory' && continuation.next) {
102
+ expansions.add('continuation');
103
+ }
104
+ return Array.from(expansions);
105
+ }
106
+ function buildStoredResult(entry, publicTool, params, summary, text) {
107
+ const context = getRouterContext();
108
+ const continuation = detectContinuation(entry.action, params, text);
109
+ const sectionHints = collectSectionHints(text);
110
+ const stored = putStoredResult({
111
+ kind: entry.responseFamily,
112
+ ownerSessionKey: context.sessionKey,
113
+ summary,
114
+ availableExpansions: buildAvailableExpansions(entry.responseFamily, continuation, sectionHints),
115
+ payload: {
116
+ recipe: {
117
+ publicTool,
118
+ action: entry.action,
119
+ params,
120
+ responseFamily: entry.responseFamily,
121
+ defaultDetail: entry.defaultDetail,
122
+ },
123
+ continuation,
124
+ sectionHints,
125
+ },
126
+ });
127
+ return stored;
128
+ }
129
+ function normalizeSuccessResponse(entry, publicTool, params, text, requestedDetail, allowHandles) {
130
+ const baseText = text.trim();
131
+ const summaryText = buildTextVariant(entry.responseFamily, 'summary', baseText);
132
+ const standardText = buildTextVariant(entry.responseFamily, 'standard', baseText);
133
+ const fullText = buildTextVariant(entry.responseFamily, 'full', baseText);
134
+ const requestedText = requestedDetail === 'summary'
135
+ ? summaryText
136
+ : requestedDetail === 'standard'
137
+ ? standardText
138
+ : fullText;
139
+ const size = estimateRenderedSize(requestedText);
140
+ const limit = getFamilyLimit(entry.responseFamily, requestedDetail);
141
+ const needsHandle = allowHandles
142
+ && isHandleEligible(entry)
143
+ && (requestedDetail === 'summary' || size > limit);
144
+ if (needsHandle) {
145
+ const stored = buildStoredResult(entry, publicTool, params, summaryText, fullText);
146
+ return mcpResultHandle(summaryText, stored.resultId, stored.availableExpansions, {
147
+ family: entry.responseFamily,
148
+ action: entry.action,
149
+ defaultDetail: entry.defaultDetail,
150
+ });
151
+ }
152
+ return mcpText(requestedText, {
153
+ family: entry.responseFamily,
154
+ action: entry.action,
155
+ detail: requestedDetail,
156
+ });
157
+ }
158
+ function normalizeActionResponse(entry, publicTool, params, result, requestedDetail, allowHandles) {
159
+ const text = toPlainText(result);
160
+ if (result.isError) {
161
+ return mcpErrorCompact(compactErrorText(text), {
162
+ action: entry.action,
163
+ publicTool,
164
+ error: true,
165
+ });
166
+ }
167
+ return normalizeSuccessResponse(entry, publicTool, params, text, requestedDetail, allowHandles);
168
+ }
169
+ async function executeActionViaRouter(publicTool, action, params, requestedDetail, extra, allowHandles) {
170
+ const entry = getActionCatalogEntry(action);
171
+ try {
172
+ const result = await callActionHandler(action, params, extra);
173
+ return normalizeActionResponse(entry, publicTool, params, result, requestedDetail, allowHandles);
174
+ }
175
+ catch (err) {
176
+ const message = err instanceof Error ? err.message : String(err);
177
+ return mcpErrorCompact(message, { code: 'HANDLER_ERROR', action, publicTool });
178
+ }
179
+ }
180
+ export async function dispatchRoutedTool(publicTool, params, extra) {
181
+ const action = params.action;
182
+ if (!action) {
183
+ return mcpErrorCompact('Missing required "action" field.', { code: 'MISSING_ACTION' });
184
+ }
185
+ const requestedDetail = pickRequestedDetail(getActionCatalogEntry(action), params.detail);
186
+ const actionParams = buildActionParams(params);
187
+ const requiredFields = ACTION_REQUIRED_PARAMS[action];
188
+ if (requiredFields) {
189
+ const missing = requiredFields.filter((f) => actionParams[f] === undefined);
190
+ if (missing.length > 0) {
191
+ return mcpErrorCompact(`Missing required parameter${missing.length > 1 ? 's' : ''} for ${action}: ${missing.join(', ')}`, { code: 'MISSING_PARAMS', action, missing });
192
+ }
193
+ }
194
+ return executeActionViaRouter(publicTool, action, actionParams, requestedDetail, extra, true);
195
+ }
196
+ function applyContinuation(params, stored, continuation) {
197
+ if (!continuation) {
198
+ return params;
199
+ }
200
+ if (continuation !== 'next') {
201
+ return {
202
+ ...params,
203
+ continuation,
204
+ };
205
+ }
206
+ if (stored.payload.continuation.model === 'transactionHistory' && stored.payload.continuation.next) {
207
+ const next = stored.payload.continuation.next;
208
+ if (next.kind === 'signaturesQuick') {
209
+ return {
210
+ ...params,
211
+ before: next.nextBefore,
212
+ };
213
+ }
214
+ return {
215
+ ...params,
216
+ paginationToken: next.paginationToken,
217
+ };
218
+ }
219
+ if (stored.payload.continuation.model === 'page' && stored.payload.continuation.nextPage !== undefined) {
220
+ return {
221
+ ...params,
222
+ page: stored.payload.continuation.nextPage,
223
+ };
224
+ }
225
+ return params;
226
+ }
227
+ export async function expandStoredResult(params, extra) {
228
+ const resultId = typeof params.resultId === 'string' ? params.resultId : '';
229
+ if (!resultId) {
230
+ return mcpErrorCompact('Missing required "resultId".', { code: 'MISSING_RESULT_ID' });
231
+ }
232
+ const context = getRouterContext();
233
+ const stored = getStoredResult(resultId, context.sessionKey);
234
+ if (!stored) {
235
+ return mcpErrorCompact('Result handle not found or no longer available. Re-run the original action.', {
236
+ code: 'RESULT_NOT_FOUND',
237
+ resultId,
238
+ });
239
+ }
240
+ let nextParams = {
241
+ ...stored.payload.recipe.params,
242
+ };
243
+ if (typeof params.page === 'number') {
244
+ nextParams.page = params.page;
245
+ }
246
+ nextParams = applyContinuation(nextParams, stored, typeof params.continuation === 'string' ? params.continuation : undefined);
247
+ const requestedDetail = (params.detail === 'summary' || params.detail === 'standard' || params.detail === 'full')
248
+ ? params.detail
249
+ : 'full';
250
+ try {
251
+ const rawResponse = await callActionHandler(stored.payload.recipe.action, nextParams, extra);
252
+ const rawText = toPlainText(rawResponse);
253
+ if (rawResponse.isError) {
254
+ return mcpErrorCompact(compactErrorText(rawText), {
255
+ code: 'EXPAND_FAILED',
256
+ action: stored.payload.recipe.action,
257
+ });
258
+ }
259
+ const selected = applyRangeSelection(applyItemSelection(applySectionSelection(rawText, typeof params.section === 'string' ? params.section : undefined), typeof params.item === 'number' ? params.item : undefined), typeof params.range === 'string' ? params.range : undefined);
260
+ const entry = getActionCatalogEntry(stored.payload.recipe.action);
261
+ if (params.continuation === 'next' || typeof params.page === 'number') {
262
+ return normalizeSuccessResponse(entry, stored.payload.recipe.publicTool, nextParams, selected, requestedDetail, true);
263
+ }
264
+ const rendered = buildTextVariant(entry.responseFamily, requestedDetail, selected);
265
+ return mcpText(rendered, {
266
+ action: stored.payload.recipe.action,
267
+ family: stored.kind,
268
+ resultId,
269
+ detail: requestedDetail,
270
+ });
271
+ }
272
+ catch (err) {
273
+ const message = err instanceof Error ? err.message : String(err);
274
+ return mcpErrorCompact(message, { code: 'EXPAND_HANDLER_ERROR', action: stored.payload.recipe.action });
275
+ }
276
+ }
@@ -0,0 +1 @@
1
+ export declare const ROUTER_INSTRUCTIONS = "Helius MCP exposes 10 public tools total: 9 routed domain tools plus `expandResult`.\n\nChoose tools by user intent, not by name similarity.\n\nRouting:\n- Account setup, API keys, signup, plans, billing: `heliusAccount`\n- Wallet-centric balances, holdings, identity, wallet history: `heliusWallet`\n- Assets, NFTs, collections, token holders: `heliusAsset`\n- Parsed transactions or wallet transaction history: `heliusTransaction`\n- Raw chain state, token accounts, stake reads, blocks, network status, priority fees: `heliusChain`\n- Webhook CRUD or live subscription configuration: `heliusStreaming`\n- Docs, guides, pricing references, troubleshooting, source, blog, SIMDs: `heliusKnowledge`\n- SOL/token transfers or staking mutations: `heliusWrite`\n- Compressed account, proof, balance, compression history: `heliusCompression`\n\nRules:\n- Use the chosen routed tool plus the Helius action name in `action`.\n- Wallet holdings use `heliusWallet.getTokenBalances`; raw token accounts use `heliusChain.getTokenAccounts`.\n- Parsed transaction details use `heliusTransaction.parseTransactions`; wallet activity listing uses `heliusTransaction.getTransactionHistory`; per-transfer rows (token + SOL with mint/direction/counterparty filters) use `heliusTransaction.getTransfersByAddress`.\n- Streaming or webhook setup guides live under `heliusKnowledge`; actual webhook/subscription config lives under `heliusStreaming`.\n- Pricing and plan selection start with `heliusAccount.getHeliusPlanInfo`; per-method credit costs or rate limits use `heliusKnowledge.getRateLimitInfo` or `heliusKnowledge.getHeliusCreditsInfo`.\n- Read queries stay on domain tools; sends and staking mutations use `heliusWrite`.\n- Heavy content is summary-first. Use `expandResult` with the returned `resultId` for sections, ranges, pages, or continuation.\n- Set `_feedback` to a short reason for the call or takeaway from the previous result. Avoid placeholders like `first_call`.\n- Set `_feedbackTool` to the current `publicTool.action`, e.g. `heliusWallet.getBalance`. Always send `_model`.";
@@ -0,0 +1,25 @@
1
+ export const ROUTER_INSTRUCTIONS = `Helius MCP exposes 10 public tools total: 9 routed domain tools plus \`expandResult\`.
2
+
3
+ Choose tools by user intent, not by name similarity.
4
+
5
+ Routing:
6
+ - Account setup, API keys, signup, plans, billing: \`heliusAccount\`
7
+ - Wallet-centric balances, holdings, identity, wallet history: \`heliusWallet\`
8
+ - Assets, NFTs, collections, token holders: \`heliusAsset\`
9
+ - Parsed transactions or wallet transaction history: \`heliusTransaction\`
10
+ - Raw chain state, token accounts, stake reads, blocks, network status, priority fees: \`heliusChain\`
11
+ - Webhook CRUD or live subscription configuration: \`heliusStreaming\`
12
+ - Docs, guides, pricing references, troubleshooting, source, blog, SIMDs: \`heliusKnowledge\`
13
+ - SOL/token transfers or staking mutations: \`heliusWrite\`
14
+ - Compressed account, proof, balance, compression history: \`heliusCompression\`
15
+
16
+ Rules:
17
+ - Use the chosen routed tool plus the Helius action name in \`action\`.
18
+ - Wallet holdings use \`heliusWallet.getTokenBalances\`; raw token accounts use \`heliusChain.getTokenAccounts\`.
19
+ - Parsed transaction details use \`heliusTransaction.parseTransactions\`; wallet activity listing uses \`heliusTransaction.getTransactionHistory\`; per-transfer rows (token + SOL with mint/direction/counterparty filters) use \`heliusTransaction.getTransfersByAddress\`.
20
+ - Streaming or webhook setup guides live under \`heliusKnowledge\`; actual webhook/subscription config lives under \`heliusStreaming\`.
21
+ - Pricing and plan selection start with \`heliusAccount.getHeliusPlanInfo\`; per-method credit costs or rate limits use \`heliusKnowledge.getRateLimitInfo\` or \`heliusKnowledge.getHeliusCreditsInfo\`.
22
+ - Read queries stay on domain tools; sends and staking mutations use \`heliusWrite\`.
23
+ - Heavy content is summary-first. Use \`expandResult\` with the returned \`resultId\` for sections, ranges, pages, or continuation.
24
+ - Set \`_feedback\` to a short reason for the call or takeaway from the previous result. Avoid placeholders like \`first_call\`.
25
+ - Set \`_feedbackTool\` to the current \`publicTool.action\`, e.g. \`heliusWallet.getBalance\`. Always send \`_model\`.`;
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerRouterTools(server: McpServer): void;
@@ -0,0 +1,15 @@
1
+ import { dispatchRoutedTool, expandStoredResult } from './dispatch.js';
2
+ import { withTelemetryHandler } from './telemetry.js';
3
+ import { EXPAND_RESULT_SCHEMA, HELIUS_ACCOUNT_SCHEMA, HELIUS_ASSET_SCHEMA, HELIUS_CHAIN_SCHEMA, HELIUS_COMPRESSION_SCHEMA, HELIUS_KNOWLEDGE_SCHEMA, HELIUS_STREAMING_SCHEMA, HELIUS_TRANSACTION_SCHEMA, HELIUS_WALLET_SCHEMA, HELIUS_WRITE_SCHEMA, } from './schemas.js';
4
+ export function registerRouterTools(server) {
5
+ server.tool('heliusAccount', 'Account setup, API keys, signup, plans, and billing. Use for pricing or account state, not per-method rate limits.', HELIUS_ACCOUNT_SCHEMA, withTelemetryHandler('heliusAccount', (params, extra) => dispatchRoutedTool('heliusAccount', params, extra)));
6
+ server.tool('heliusWallet', 'Wallet-centric balances, holdings, identity, and wallet history. Use for portfolio views, not raw token accounts.', HELIUS_WALLET_SCHEMA, withTelemetryHandler('heliusWallet', (params, extra) => dispatchRoutedTool('heliusWallet', params, extra)));
7
+ server.tool('heliusAsset', 'Assets, NFTs, collections, proofs, and token holders. Use for DAS ownership or metadata, not transaction history.', HELIUS_ASSET_SCHEMA, withTelemetryHandler('heliusAsset', (params, extra) => dispatchRoutedTool('heliusAsset', params, extra)));
8
+ server.tool('heliusTransaction', 'Parsed transactions and wallet transaction history. Use for activity analysis, not raw account state.', HELIUS_TRANSACTION_SCHEMA, withTelemetryHandler('heliusTransaction', (params, extra) => dispatchRoutedTool('heliusTransaction', params, extra)));
9
+ server.tool('heliusChain', 'Raw chain state, token accounts, stake reads, blocks, network status, and priority fees. Use for token accounts or blocks, not wallet portfolio summaries.', HELIUS_CHAIN_SCHEMA, withTelemetryHandler('heliusChain', (params, extra) => dispatchRoutedTool('heliusChain', params, extra)));
10
+ server.tool('heliusStreaming', 'Webhook CRUD and live subscription configuration. Use for actual webhook/subscription setup, not how-to guides.', HELIUS_STREAMING_SCHEMA, withTelemetryHandler('heliusStreaming', (params, extra) => dispatchRoutedTool('heliusStreaming', params, extra)));
11
+ server.tool('heliusKnowledge', 'Docs, guides, pricing references, troubleshooting, source, blog, and SIMD research. Use for guides, rate limits, or errors, not live mutations.', HELIUS_KNOWLEDGE_SCHEMA, withTelemetryHandler('heliusKnowledge', (params, extra) => dispatchRoutedTool('heliusKnowledge', params, extra)));
12
+ server.tool('heliusWrite', 'Mutating SOL/token transfer and staking actions. Use for sends or staking, not read-only queries.', HELIUS_WRITE_SCHEMA, withTelemetryHandler('heliusWrite', (params, extra) => dispatchRoutedTool('heliusWrite', params, extra)));
13
+ server.tool('heliusCompression', 'Compressed account, proof, balance, and compression history queries. Use for zk-compression state, not standard DAS assets.', HELIUS_COMPRESSION_SCHEMA, withTelemetryHandler('heliusCompression', (params, extra) => dispatchRoutedTool('heliusCompression', params, extra)));
14
+ server.tool('expandResult', 'Expand a prior summary-first result by resultId, section, range, page, or continuation.', EXPAND_RESULT_SCHEMA, withTelemetryHandler('expandResult', (params, extra) => expandStoredResult(params, extra)));
15
+ }
@@ -0,0 +1,9 @@
1
+ import type { ActionName } from './actions.js';
2
+ /**
3
+ * Map of actions to their required top-level parameters.
4
+ *
5
+ * Only actions with unambiguous required fields are listed here.
6
+ * Actions with "one-of" requirements (e.g. getAsset needs `id` OR `ids`)
7
+ * or no required params are omitted — those rely on bespoke handler validation.
8
+ */
9
+ export declare const ACTION_REQUIRED_PARAMS: Partial<Record<ActionName, string[]>>;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Map of actions to their required top-level parameters.
3
+ *
4
+ * Only actions with unambiguous required fields are listed here.
5
+ * Actions with "one-of" requirements (e.g. getAsset needs `id` OR `ids`)
6
+ * or no required params are omitted — those rely on bespoke handler validation.
7
+ */
8
+ export const ACTION_REQUIRED_PARAMS = {
9
+ // Wallet
10
+ getBalance: ['address'],
11
+ getTokenBalances: ['address'],
12
+ getWalletBalances: ['address'],
13
+ getWalletHistory: ['address'],
14
+ getWalletTransfers: ['address'],
15
+ getWalletIdentity: ['address'],
16
+ batchWalletIdentity: ['addresses'],
17
+ getWalletFundedBy: ['address'],
18
+ // Transaction
19
+ parseTransactions: ['signatures'],
20
+ getTransactionHistory: ['address'],
21
+ getTransfersByAddress: ['address'],
22
+ // Asset
23
+ getAssetsByOwner: ['address'],
24
+ getAssetsByGroup: ['groupKey', 'groupValue'],
25
+ getAssetProof: ['id'],
26
+ getAssetProofBatch: ['ids'],
27
+ getSignaturesForAsset: ['id'],
28
+ getNftEditions: ['mint'],
29
+ // Chain
30
+ getTokenHolders: ['mint'],
31
+ getProgramAccounts: ['programId'],
32
+ getBlock: ['slot'],
33
+ getWithdrawableAmount: ['stakeAccount'],
34
+ // Streaming
35
+ createWebhook: ['webhookURL', 'accountAddresses'],
36
+ getWebhookByID: ['webhookID'],
37
+ updateWebhook: ['webhookID'],
38
+ deleteWebhook: ['webhookID'],
39
+ accountSubscribe: ['account'],
40
+ // Knowledge
41
+ troubleshootError: ['errorCode'],
42
+ recommendStack: ['description'],
43
+ getSIMD: ['number'],
44
+ searchSolanaDocs: ['query'],
45
+ readSolanaSourceFile: ['path'],
46
+ compareHeliusPlans: ['category'],
47
+ // Write
48
+ transferSol: ['recipientAddress'],
49
+ transferToken: ['recipientAddress', 'mintAddress'],
50
+ stakeSOL: ['amount'],
51
+ unstakeSOL: ['stakeAccount'],
52
+ withdrawStake: ['stakeAccount'],
53
+ // Compression
54
+ getCompressedAccountsByOwner: ['owner'],
55
+ getCompressedBalanceByOwner: ['owner'],
56
+ getCompressedMintTokenHolders: ['mint'],
57
+ getCompressedTokenAccountsByOwner: ['owner'],
58
+ getCompressedTokenAccountsByDelegate: ['delegate'],
59
+ getCompressedTokenBalancesByOwnerV2: ['owner'],
60
+ getCompressedAccountProof: ['hash'],
61
+ getCompressionSignaturesForAccount: ['hash'],
62
+ getCompressionSignaturesForAddress: ['address'],
63
+ getCompressionSignaturesForOwner: ['owner'],
64
+ getCompressionSignaturesForTokenOwner: ['owner'],
65
+ getTransactionWithCompressionInfo: ['signature'],
66
+ };
@@ -0,0 +1,29 @@
1
+ import type { DetailLevel, ResponseFamily } from './types.js';
2
+ export type RouterResponse = {
3
+ content: Array<{
4
+ type: 'text';
5
+ text: string;
6
+ }>;
7
+ isError?: boolean;
8
+ _meta?: Record<string, unknown>;
9
+ };
10
+ export declare function getFamilyLimit(family: ResponseFamily, detail: DetailLevel): number;
11
+ export declare function toPlainText(response: {
12
+ content?: Array<{
13
+ type?: string;
14
+ text?: string;
15
+ }>;
16
+ structuredContent?: unknown;
17
+ }): string;
18
+ export declare function compactErrorText(text: string, maxChars?: number): string;
19
+ export declare function cleanText(text: string): string;
20
+ export declare function truncateText(text: string, limit: number): string;
21
+ export declare function buildTextVariant(family: ResponseFamily, detail: DetailLevel, text: string): string;
22
+ export declare function estimateRenderedSize(text: string): number;
23
+ export declare function collectSectionHints(text: string): string[];
24
+ export declare function applySectionSelection(text: string, section?: string): string;
25
+ export declare function applyItemSelection(text: string, item?: number): string;
26
+ export declare function applyRangeSelection(text: string, range?: string): string;
27
+ export declare function mcpText(text: string, meta?: Record<string, unknown>): RouterResponse;
28
+ export declare function mcpErrorCompact(text: string, meta?: Record<string, unknown>): RouterResponse;
29
+ export declare function mcpResultHandle(summary: string, resultId: string, availableExpansions: string[], meta?: Record<string, unknown>): RouterResponse;
@@ -0,0 +1,186 @@
1
+ import { extractSections } from '../utils/docs.js';
2
+ const FAMILY_LIMITS = {
3
+ scalar: { summary: 800, standard: 1200, full: 1200 },
4
+ mutationReceipt: { summary: 1000, standard: 1500, full: 1500 },
5
+ record: { summary: 1500, standard: 3000, full: 8000 },
6
+ list: { summary: 2500, standard: 4500, full: 12000 },
7
+ history: { summary: 2500, standard: 4500, full: 12000 },
8
+ document: { summary: 2500, standard: 5000, full: 12000 },
9
+ streamingConfig: { summary: 2500, standard: 5000, full: 12000 },
10
+ catalog: { summary: 2500, standard: 5000, full: 12000 },
11
+ };
12
+ export function getFamilyLimit(family, detail) {
13
+ return FAMILY_LIMITS[family][detail];
14
+ }
15
+ export function toPlainText(response) {
16
+ const text = response.content
17
+ ?.filter((item) => item.type === 'text' || item.type === undefined)
18
+ .map((item) => item.text ?? '')
19
+ .join('\n\n')
20
+ .trim();
21
+ if (text) {
22
+ return text;
23
+ }
24
+ if (response.structuredContent !== undefined) {
25
+ return JSON.stringify(response.structuredContent, null, 2);
26
+ }
27
+ return '';
28
+ }
29
+ export function compactErrorText(text, maxChars = 1200) {
30
+ const withoutMeta = text
31
+ .replace(/^```json[\s\S]*?```\n*/i, '')
32
+ .replace(/\n{3,}/g, '\n\n')
33
+ .trim();
34
+ return truncateText(withoutMeta, maxChars);
35
+ }
36
+ export function cleanText(text) {
37
+ return text
38
+ .replace(/\r\n/g, '\n')
39
+ .replace(/\n{3,}/g, '\n\n')
40
+ .trim();
41
+ }
42
+ export function truncateText(text, limit) {
43
+ if (text.length <= limit) {
44
+ return text;
45
+ }
46
+ const truncated = text.slice(0, limit);
47
+ const boundary = Math.max(truncated.lastIndexOf('\n## '), truncated.lastIndexOf('\n### '), truncated.lastIndexOf('\n---\n'), truncated.lastIndexOf('\n\n'));
48
+ const cut = boundary > limit * 0.55 ? boundary : limit;
49
+ return `${truncated.slice(0, cut).trim()}\n\n[truncated]`;
50
+ }
51
+ function stripExampleCode(text) {
52
+ const markers = ['**Example Code:**', '**Example:**', '```typescript', '```ts', '```javascript', '```js'];
53
+ let cut = text.length;
54
+ for (const marker of markers) {
55
+ const index = text.indexOf(marker);
56
+ if (index >= 0) {
57
+ cut = Math.min(cut, index);
58
+ }
59
+ }
60
+ return cut === text.length ? text : text.slice(0, cut).trim();
61
+ }
62
+ function summarizeDocument(text, limit) {
63
+ const headings = Array.from(text.matchAll(/^#{1,3}\s+.+$/gm))
64
+ .map((match) => match[0])
65
+ .slice(0, 5);
66
+ const intro = text
67
+ .split('\n\n')
68
+ .map((part) => part.trim())
69
+ .filter(Boolean)
70
+ .slice(0, 3)
71
+ .join('\n\n');
72
+ const body = headings.length > 0 ? `${headings.join('\n')}\n\n${intro}` : intro || text;
73
+ return truncateText(body, limit);
74
+ }
75
+ function summarizeListLike(text, limit) {
76
+ const sections = text.split(/\n---\n|\n\n(?=\*\*|\p{Emoji_Presentation}|\p{Emoji}\s)/u);
77
+ const picked = [];
78
+ let current = 0;
79
+ for (const section of sections) {
80
+ const trimmed = section.trim();
81
+ if (!trimmed) {
82
+ continue;
83
+ }
84
+ const nextLength = current + trimmed.length + 2;
85
+ if (picked.length > 0 && nextLength > limit) {
86
+ break;
87
+ }
88
+ picked.push(trimmed);
89
+ current = nextLength;
90
+ }
91
+ const summary = picked.join('\n\n');
92
+ return truncateText(summary || text, limit);
93
+ }
94
+ export function buildTextVariant(family, detail, text) {
95
+ const limit = getFamilyLimit(family, detail);
96
+ const cleaned = cleanText(text);
97
+ if (!cleaned) {
98
+ return '';
99
+ }
100
+ if (detail === 'full') {
101
+ return truncateText(cleaned, limit);
102
+ }
103
+ switch (family) {
104
+ case 'streamingConfig':
105
+ return truncateText(stripExampleCode(cleaned), limit);
106
+ case 'document':
107
+ return summarizeDocument(stripExampleCode(cleaned), limit);
108
+ case 'list':
109
+ case 'history':
110
+ case 'catalog':
111
+ return summarizeListLike(stripExampleCode(cleaned), limit);
112
+ case 'scalar':
113
+ case 'mutationReceipt':
114
+ case 'record':
115
+ default:
116
+ return truncateText(stripExampleCode(cleaned), limit);
117
+ }
118
+ }
119
+ export function estimateRenderedSize(text) {
120
+ return cleanText(text).length;
121
+ }
122
+ export function collectSectionHints(text) {
123
+ return Array.from(text.matchAll(/^#{1,3}\s+(.+)$/gm))
124
+ .map((match) => match[1].trim())
125
+ .filter(Boolean)
126
+ .slice(0, 12);
127
+ }
128
+ export function applySectionSelection(text, section) {
129
+ if (!section) {
130
+ return text;
131
+ }
132
+ const extracted = extractSections(text, section, { includeLooseMatches: true });
133
+ if (extracted) {
134
+ return extracted;
135
+ }
136
+ return `No section matching "${section}" was found.\n\n${text}`;
137
+ }
138
+ export function applyItemSelection(text, item) {
139
+ if (item === undefined) {
140
+ return text;
141
+ }
142
+ const sections = text
143
+ .split(/\n---\n|\n\n(?=\*\*|\p{Emoji_Presentation}|\p{Emoji}\s)/u)
144
+ .map((part) => part.trim())
145
+ .filter(Boolean);
146
+ const index = item - 1;
147
+ if (index < 0 || index >= sections.length) {
148
+ return `Item ${item} was not found.\n\n${text}`;
149
+ }
150
+ return sections[index];
151
+ }
152
+ export function applyRangeSelection(text, range) {
153
+ if (!range) {
154
+ return text;
155
+ }
156
+ const match = range.match(/^(\d+):(\d+)$/);
157
+ if (!match) {
158
+ return `Invalid range "${range}". Expected "start:end".\n\n${text}`;
159
+ }
160
+ const start = Number(match[1]);
161
+ const end = Number(match[2]);
162
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end <= start) {
163
+ return `Invalid range "${range}". Expected "start:end" with start >= 0 and end > start.\n\n${text}`;
164
+ }
165
+ return text.slice(start, end);
166
+ }
167
+ export function mcpText(text, meta) {
168
+ return {
169
+ content: [{ type: 'text', text }],
170
+ ...(meta === undefined ? {} : { _meta: meta }),
171
+ };
172
+ }
173
+ export function mcpErrorCompact(text, meta) {
174
+ return {
175
+ content: [{ type: 'text', text }],
176
+ isError: true,
177
+ ...(meta === undefined ? {} : { _meta: meta }),
178
+ };
179
+ }
180
+ export function mcpResultHandle(summary, resultId, availableExpansions, meta) {
181
+ const lines = [summary, '', `resultId: ${resultId}`];
182
+ if (availableExpansions.length > 0) {
183
+ lines.push(`expand: ${availableExpansions.join(', ')}`);
184
+ }
185
+ return mcpText(lines.join('\n'), meta);
186
+ }