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
@@ -6,7 +6,7 @@ interface HeliusConfig {
6
6
  network?: string;
7
7
  projectId?: string;
8
8
  preferences?: {
9
- budget?: 'free' | 'developer' | 'business' | 'professional';
9
+ budget?: 'agent' | 'developer' | 'business' | 'professional';
10
10
  complexity?: 'low' | 'medium' | 'high';
11
11
  };
12
12
  }
@@ -17,7 +17,7 @@ export declare function setSharedApiKey(apiKey: string): void;
17
17
  export declare function getJwt(): string | undefined;
18
18
  export declare function setJwt(jwt: string): void;
19
19
  export declare function getPreferences(): {
20
- budget?: "free" | "developer" | "business" | "professional";
20
+ budget?: "agent" | "developer" | "business" | "professional";
21
21
  complexity?: "low" | "medium" | "high";
22
22
  };
23
23
  export declare function savePreferences(prefs: HeliusConfig['preferences']): void;
@@ -9,17 +9,79 @@ function ensureDir() {
9
9
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
10
10
  }
11
11
  }
12
+ /** Best-effort recovery when config.json is corrupted (matches helius-cli behavior). */
13
+ function recoverPartialConfig(raw) {
14
+ const recovered = {};
15
+ const patterns = [
16
+ { key: "jwt", regex: /"jwt"\s*:\s*"([^"]+)"/ },
17
+ { key: "apiKey", regex: /"apiKey"\s*:\s*"([^"]+)"/ },
18
+ {
19
+ key: "network",
20
+ regex: /"network"\s*:\s*"(mainnet|mainnet-beta|devnet)"/,
21
+ },
22
+ { key: "projectId", regex: /"projectId"\s*:\s*"([^"]+)"/ },
23
+ ];
24
+ for (const { key, regex } of patterns) {
25
+ const match = raw.match(regex);
26
+ if (match) {
27
+ recovered[key] = match[1];
28
+ }
29
+ }
30
+ const budgetMatch = raw.match(/"budget"\s*:\s*"(free|developer|business|professional)"/);
31
+ const complexityMatch = raw.match(/"complexity"\s*:\s*"(low|medium|high)"/);
32
+ if (budgetMatch || complexityMatch) {
33
+ recovered.preferences = {};
34
+ if (budgetMatch) {
35
+ recovered.preferences.budget = budgetMatch[1];
36
+ }
37
+ if (complexityMatch) {
38
+ recovered.preferences.complexity = complexityMatch[1];
39
+ }
40
+ }
41
+ return recovered;
42
+ }
43
+ function recoveredFieldSummary(cfg) {
44
+ const keys = [];
45
+ for (const k of Object.keys(cfg)) {
46
+ if (k === "preferences" && cfg.preferences) {
47
+ for (const pk of Object.keys(cfg.preferences)) {
48
+ keys.push(`preferences.${pk}`);
49
+ }
50
+ }
51
+ else if (k !== "preferences") {
52
+ keys.push(k);
53
+ }
54
+ }
55
+ return keys.join(", ");
56
+ }
12
57
  export function loadConfig() {
58
+ if (!fs.existsSync(SHARED_CONFIG_PATH)) {
59
+ return {};
60
+ }
61
+ let raw;
13
62
  try {
14
- if (fs.existsSync(SHARED_CONFIG_PATH)) {
15
- const data = fs.readFileSync(SHARED_CONFIG_PATH, "utf-8");
16
- return JSON.parse(data);
17
- }
63
+ raw = fs.readFileSync(SHARED_CONFIG_PATH, "utf-8");
18
64
  }
19
65
  catch {
20
- // Return empty config on error
66
+ console.error(`Warning: ${SHARED_CONFIG_PATH} exists but could not be read.`);
67
+ return {};
68
+ }
69
+ try {
70
+ return JSON.parse(raw);
71
+ }
72
+ catch {
73
+ const recovered = recoverPartialConfig(raw);
74
+ const summary = recoveredFieldSummary(recovered);
75
+ if (summary.length > 0) {
76
+ console.error(`Warning: ${SHARED_CONFIG_PATH} is corrupted (invalid JSON). Recovered fields: ${summary}.`);
77
+ saveConfig(recovered);
78
+ console.error("Repaired config saved. Fields that did not match known keys may have been lost.");
79
+ return recovered;
80
+ }
81
+ console.error(`Warning: ${SHARED_CONFIG_PATH} is corrupted (invalid JSON) and could not be recovered.`);
82
+ console.error('Run Helius MCP config helpers or edit the file manually; see CLI `helius config clear` if you use helius-cli.');
83
+ return {};
21
84
  }
22
- return {};
23
85
  }
24
86
  export function saveConfig(config) {
25
87
  ensureDir();
@@ -5,8 +5,14 @@ type McpToolResponse = {
5
5
  }];
6
6
  isError?: boolean;
7
7
  };
8
+ export type ErrorMeta = {
9
+ type: 'VALIDATION' | 'NOT_FOUND' | 'AUTH' | 'RATE_LIMIT' | 'INSUFFICIENT_FUNDS' | 'UNSUPPORTED' | 'HTTP' | 'API';
10
+ code: string;
11
+ retryable: boolean;
12
+ recovery: string;
13
+ };
8
14
  export declare function mcpText(text: string): McpToolResponse;
9
- export declare function mcpError(text: string): McpToolResponse;
15
+ export declare function mcpError(text: string, meta?: ErrorMeta): McpToolResponse;
10
16
  export declare function getErrorMessage(err: unknown): string;
11
17
  export declare function isAddressError(msg: string): boolean;
12
18
  export declare function isPaginationError(msg: string): boolean;
@@ -29,4 +35,7 @@ export declare function isValidAddressFormat(address: string): boolean;
29
35
  export declare function warnInvalidAddresses(paramName: string, addresses: string[]): string[];
30
36
  export declare function warnInvalidAddress(paramName: string, address: string): string | null;
31
37
  export declare function warnAddressConflicts(includeName: string, includeAddresses: string[] | undefined, excludeName: string, excludeAddresses: string[] | undefined): string | null;
38
+ export declare function missingParamError(toolName: string, recovery: string): McpToolResponse;
39
+ export declare function exclusiveParamError(toolName: string, paramA: string, paramB: string): McpToolResponse;
40
+ export declare function batchLimitError(toolName: string, max: number, actual: number): McpToolResponse;
32
41
  export type { ErrorHandler, McpToolResponse };
@@ -2,8 +2,11 @@
2
2
  export function mcpText(text) {
3
3
  return { content: [{ type: 'text', text }] };
4
4
  }
5
- export function mcpError(text) {
6
- return { content: [{ type: 'text', text }], isError: true };
5
+ export function mcpError(text, meta) {
6
+ const body = meta
7
+ ? '```json\n' + JSON.stringify(meta) + '\n```\n\n' + text
8
+ : text;
9
+ return { content: [{ type: 'text', text: body }], isError: true };
7
10
  }
8
11
  // ─── Error Message Extraction ───
9
12
  export function getErrorMessage(err) {
@@ -48,14 +51,14 @@ export function parseHttp400Messages(msg) {
48
51
  // ─── Enum Validation ───
49
52
  export function validateEnum(value, validOptions, context, fieldName) {
50
53
  if (!validOptions.includes(value)) {
51
- return mcpText(`**${context}**\n\nInvalid ${fieldName} "${value}". Valid options: ${validOptions.join(', ')}`);
54
+ return mcpError(`**${context}**\n\nInvalid ${fieldName} "${value}". Valid options: ${validOptions.join(', ')}`, { type: 'VALIDATION', code: 'INVALID_ENUM', retryable: false, recovery: `Use one of: ${validOptions.join(', ')}` });
52
55
  }
53
56
  return null;
54
57
  }
55
58
  // ─── HTTP Status Guidance ───
56
59
  const HTTP_GUIDANCE = {
57
60
  '401': 'API key is invalid or expired. Call `setHeliusApiKey` with a valid key, or call `getAccountStatus` to check your current auth state.',
58
- '403': 'This endpoint is restricted on your current plan. Call `getAccountStatus` to check your plan tier and remaining credits. Some endpoints (Enhanced Transactions, Token API) require Developer plan or higher. Call `getHeliusPlanInfo` to compare plans, or `previewUpgrade` to see upgrade pricing.',
61
+ '403': 'This endpoint is restricted on your current plan. Call `getAccountPlan` for a quick plan/feature check, or `getAccountStatus` for full details. Some endpoints require Developer plan or higher. Call `getHeliusPlanInfo` to compare plans, or `previewUpgrade` to see upgrade pricing.',
59
62
  '429': 'Rate limited. Call `getAccountStatus` to check your remaining credits and rate limits. Back off and retry, or call `previewUpgrade` to see upgrade options for higher limits.',
60
63
  '502': 'Backend temporarily unavailable. Retry after a few seconds.',
61
64
  '504': 'Gateway timeout — the request took too long. Try reducing the query scope (fewer addresses, smaller limit, narrower time range).',
@@ -68,6 +71,21 @@ function extractHttpGuidance(msg) {
68
71
  }
69
72
  return null;
70
73
  }
74
+ const HTTP_META = {
75
+ '401': { type: 'AUTH', code: 'HTTP_401', retryable: false, recovery: HTTP_GUIDANCE['401'] },
76
+ '403': { type: 'AUTH', code: 'HTTP_403', retryable: false, recovery: HTTP_GUIDANCE['403'] },
77
+ '429': { type: 'RATE_LIMIT', code: 'HTTP_429', retryable: true, recovery: HTTP_GUIDANCE['429'] },
78
+ '502': { type: 'HTTP', code: 'HTTP_502', retryable: true, recovery: HTTP_GUIDANCE['502'] },
79
+ '504': { type: 'HTTP', code: 'HTTP_504', retryable: true, recovery: HTTP_GUIDANCE['504'] },
80
+ };
81
+ function extractHttpMeta(msg, _guidance) {
82
+ for (const [status, meta] of Object.entries(HTTP_META)) {
83
+ if (msg.includes(`HTTP ${status}`) || msg.includes(`status: ${status}`) || msg.includes(`(${status})`)) {
84
+ return meta;
85
+ }
86
+ }
87
+ return { type: 'API', code: 'UNKNOWN', retryable: false, recovery: 'Check the error message for details' };
88
+ }
71
89
  export function handleToolError(err, fallbackPrefix, handlers) {
72
90
  const msg = getErrorMessage(err);
73
91
  if (handlers) {
@@ -78,33 +96,37 @@ export function handleToolError(err, fallbackPrefix, handlers) {
78
96
  }
79
97
  const guidance = extractHttpGuidance(msg);
80
98
  if (guidance) {
81
- return mcpError(`**${fallbackPrefix}:** ${msg}\n\n${guidance}`);
99
+ const meta = extractHttpMeta(msg, guidance);
100
+ return mcpError(`**${fallbackPrefix}:** ${msg}\n\n${guidance}`, meta);
82
101
  }
83
- return mcpError(`**${fallbackPrefix}:** ${msg}`);
102
+ return mcpError(`**${fallbackPrefix}:** ${msg}`, { type: 'API', code: 'UNKNOWN', retryable: false, recovery: 'Check the error message for details' });
84
103
  }
85
104
  // ─── Pre-built Handler Factories ───
86
105
  export function addressError(header, detail) {
106
+ const recovery = detail || 'Invalid Solana address. Please provide a valid base58-encoded address.';
87
107
  return {
88
108
  match: isAddressError,
89
- respond: () => mcpText(`**${header}**\n\n${detail || 'Invalid Solana address. Please provide a valid base58-encoded address.'}`),
109
+ respond: () => mcpError(`**${header}**\n\n${recovery}`, { type: 'VALIDATION', code: 'INVALID_ADDRESS', retryable: false, recovery }),
90
110
  };
91
111
  }
92
112
  export function paginationError(header, detail) {
113
+ const recovery = detail || 'Invalid pagination parameters. Page must be at least 1 and limit must be between 1 and 1000.';
93
114
  return {
94
115
  match: isPaginationError,
95
- respond: () => mcpText(`**${header}**\n\n${detail || 'Invalid pagination parameters. Page must be at least 1 and limit must be between 1 and 1000.'}`),
116
+ respond: () => mcpError(`**${header}**\n\n${recovery}`, { type: 'VALIDATION', code: 'INVALID_PAGINATION', retryable: false, recovery }),
96
117
  };
97
118
  }
98
119
  export function notFoundError(header, detail) {
120
+ const recovery = detail || 'Asset not found. This mint address does not exist or has not been indexed.';
99
121
  return {
100
122
  match: isNotFoundError,
101
- respond: () => mcpText(`**${header}**\n\n${detail || 'Asset not found. This mint address does not exist or has not been indexed.'}`),
123
+ respond: () => mcpError(`**${header}**\n\n${recovery}`, { type: 'NOT_FOUND', code: 'RESOURCE_NOT_FOUND', retryable: false, recovery }),
102
124
  };
103
125
  }
104
126
  export function http404Error(header, detail) {
105
127
  return {
106
128
  match: isHttp404Error,
107
- respond: () => header ? mcpText(`**${header}**\n\n${detail}`) : mcpText(detail),
129
+ respond: () => mcpError(header ? `**${header}**\n\n${detail}` : detail, { type: 'NOT_FOUND', code: 'HTTP_404', retryable: false, recovery: detail }),
108
130
  };
109
131
  }
110
132
  export function http400Error(header) {
@@ -113,9 +135,9 @@ export function http400Error(header) {
113
135
  respond: (msg) => {
114
136
  const messages = parseHttp400Messages(msg);
115
137
  if (messages) {
116
- return mcpText(`**${header}**\n\n${messages.map((m) => `- ${m}`).join('\n')}`);
138
+ return mcpError(`**${header}**\n\n${messages.map((m) => `- ${m}`).join('\n')}`, { type: 'API', code: 'BAD_REQUEST', retryable: false, recovery: 'Fix the request parameters' });
117
139
  }
118
- return mcpError(`**${header}:** ${msg}`);
140
+ return mcpError(`**${header}:** ${msg}`, { type: 'API', code: 'BAD_REQUEST', retryable: false, recovery: 'Fix the request parameters' });
119
141
  },
120
142
  };
121
143
  }
@@ -155,3 +177,15 @@ export function warnAddressConflicts(includeName, includeAddresses, excludeName,
155
177
  }
156
178
  return null;
157
179
  }
180
+ // ─── Convenience Error Helpers ───
181
+ export function missingParamError(toolName, recovery) {
182
+ return mcpError(`**${toolName} Error**\n\n${recovery}`, { type: 'VALIDATION', code: 'MISSING_PARAM', retryable: false, recovery });
183
+ }
184
+ export function exclusiveParamError(toolName, paramA, paramB) {
185
+ const recovery = `Provide either \`${paramA}\` or \`${paramB}\`, not both.`;
186
+ return mcpError(`**${toolName} Error**\n\n${recovery}`, { type: 'VALIDATION', code: 'EXCLUSIVE_PARAMS', retryable: false, recovery });
187
+ }
188
+ export function batchLimitError(toolName, max, actual) {
189
+ const recovery = `Reduce batch to ${max} or fewer items.`;
190
+ return mcpError(`**${toolName} Error**\n\nMaximum ${max} items per request. You provided ${actual}.`, { type: 'VALIDATION', code: 'TOO_MANY_ITEMS', retryable: false, recovery });
191
+ }
@@ -8,7 +8,6 @@ const POSTHOG_API_KEY = 'phc_aLmID5mMwUZi3pVhG4HomDeZaWZ1PqAEkWTempDogzi';
8
8
  const HELIUS_DIR = path.join(os.homedir(), '.helius');
9
9
  const ANON_ID_PATH = path.join(HELIUS_DIR, 'anon-id');
10
10
  let clientInfo = null;
11
- let feedbackEnabled = true;
12
11
  let walletAddress = null;
13
12
  let identifySent = false;
14
13
  // Persistent anonymous ID shared with helius-cli.
@@ -47,8 +46,6 @@ function getDistinctId() {
47
46
  return walletAddress || sessionId;
48
47
  }
49
48
  function posthogCapture(event, properties) {
50
- if (!feedbackEnabled)
51
- return;
52
49
  fetch(POSTHOG_ENDPOINT, {
53
50
  method: 'POST',
54
51
  headers: { 'Content-Type': 'application/json' },
@@ -58,7 +55,7 @@ function posthogCapture(event, properties) {
58
55
  properties,
59
56
  }),
60
57
  }).catch(() => {
61
- feedbackEnabled = false;
58
+ /* ignore delivery failure */
62
59
  });
63
60
  }
64
61
  export function sendFeedbackEvent(event) {
@@ -59,7 +59,8 @@ export function getEnhancedWebSocketUrl() {
59
59
  return `wss://atlas-mainnet.helius-rpc.com/?api-key=${apiKey}`;
60
60
  }
61
61
  export function getLaserstreamUrl(region) {
62
- const apiKey = getApiKey();
62
+ // Endpoint host is public; clients pass apiKey separately (e.g. @helius/laserstream subscribe options).
63
+ // Do not call getApiKey() here or docs tools like getLaserstreamInfo fail unnecessarily.
63
64
  const network = getNetwork();
64
65
  if (network === 'devnet') {
65
66
  return `https://laserstream-devnet-ewr.helius-rpc.com`;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Open Wallet Standard (OWS) integration.
3
+ *
4
+ * Provides an optional, additive signing path that delegates to the `ows` CLI
5
+ * for policy-gated, key-isolated transaction signing. When an `owsWallet`
6
+ * parameter is supplied, transfers and staking tools use an OWS-backed signer
7
+ * instead of the local keypair. If OWS is not installed, callers get a clear
8
+ * error — existing keypair flows are unaffected.
9
+ *
10
+ * The adapter implements the same duck-typed signer interface that @solana/kit
11
+ * expects ({ address, signTransactions, signMessages }), so it plugs directly
12
+ * into the Helius SDK's sendTransactionWithSender / sendSmartTransaction
13
+ * without losing priority-fee estimation, SWQoS routing, or Jito tips.
14
+ *
15
+ * NOTE: The CLI helpers (isOwsInstalled, getOwsSolanaAddress) are mirrored in
16
+ * helius-cli/src/lib/ows.ts. Keep the two copies in sync when changing
17
+ * argument handling, CAIP-2 lookup logic, or OWS CLI flags.
18
+ */
19
+ import { type Address } from '@solana/kit';
20
+ import { mcpError } from './errors.js';
21
+ /**
22
+ * True when the `ows` binary is reachable on PATH.
23
+ */
24
+ export declare function isOwsInstalled(): Promise<boolean>;
25
+ /**
26
+ * Return the Solana address for the named OWS wallet.
27
+ * Uses the current MCP session network (mainnet or devnet) to select
28
+ * the correct CAIP-2 chain key.
29
+ */
30
+ export declare function getOwsSolanaAddress(walletName: string): Promise<string>;
31
+ /**
32
+ * A duck-typed @solana/kit TransactionPartialSigner backed by OWS.
33
+ *
34
+ * Passes `isTransactionSigner()` checks and works with
35
+ * `setTransactionMessageFeePayerSigner`, `signTransactionMessageWithSigners`,
36
+ * `sendTransactionWithSender`, and `sendSmartTransaction`.
37
+ *
38
+ * IMPORTANT: `signTransactions` and `signMessages` return **partial signature
39
+ * maps** (`{ [address]: signatureBytes }`), NOT full transaction/message objects.
40
+ * This matches the contract that `@solana/kit`'s `signTransactionMessageWithSigners`
41
+ * expects — it merges the partial maps back into the compiled transaction itself.
42
+ */
43
+ export interface OwsSigner {
44
+ address: Address;
45
+ signTransactions: (transactions: readonly {
46
+ messageBytes: Uint8Array;
47
+ }[]) => Promise<readonly Record<string, Uint8Array>[]>;
48
+ signMessages: (messages: readonly {
49
+ content: Uint8Array;
50
+ }[]) => Promise<readonly Record<string, Uint8Array>[]>;
51
+ }
52
+ /**
53
+ * Resolve a signer from an OWS wallet name or the local Helius keypair.
54
+ * Returns a result-union so callers can return the MCP error directly.
55
+ *
56
+ * The returned `signer` is typed as `any` to avoid @solana/kit branded-type
57
+ * friction — it satisfies `isTransactionSigner()` at runtime.
58
+ *
59
+ * ⚠ If @solana/kit changes its signer interface (signTransactions /
60
+ * signMessages argument or return shapes), these casts will pass type-checking
61
+ * but fail at runtime. After any @solana/kit upgrade, verify the OWS signer
62
+ * still passes `isTransactionSigner()` and produces valid signatures in the
63
+ * full `signTransactionMessageWithSigners` pipeline.
64
+ */
65
+ export declare function resolveOwsOrKeypairSigner(owsWallet?: string): Promise<{
66
+ ok: true;
67
+ signer: any;
68
+ walletAddress: string;
69
+ owsWallet?: string;
70
+ } | {
71
+ ok: false;
72
+ error: ReturnType<typeof mcpError>;
73
+ }>;
74
+ export declare function createOwsSigner(walletName: string): Promise<OwsSigner>;
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Open Wallet Standard (OWS) integration.
3
+ *
4
+ * Provides an optional, additive signing path that delegates to the `ows` CLI
5
+ * for policy-gated, key-isolated transaction signing. When an `owsWallet`
6
+ * parameter is supplied, transfers and staking tools use an OWS-backed signer
7
+ * instead of the local keypair. If OWS is not installed, callers get a clear
8
+ * error — existing keypair flows are unaffected.
9
+ *
10
+ * The adapter implements the same duck-typed signer interface that @solana/kit
11
+ * expects ({ address, signTransactions, signMessages }), so it plugs directly
12
+ * into the Helius SDK's sendTransactionWithSender / sendSmartTransaction
13
+ * without losing priority-fee estimation, SWQoS routing, or Jito tips.
14
+ *
15
+ * NOTE: The CLI helpers (isOwsInstalled, getOwsSolanaAddress) are mirrored in
16
+ * helius-cli/src/lib/ows.ts. Keep the two copies in sync when changing
17
+ * argument handling, CAIP-2 lookup logic, or OWS CLI flags.
18
+ */
19
+ import { execFile } from 'node:child_process';
20
+ import { promisify } from 'node:util';
21
+ import { address, createKeyPairSignerFromBytes } from '@solana/kit';
22
+ import { loadSignerOrFail, getNetwork } from './helius.js';
23
+ import { mcpError } from './errors.js';
24
+ const execFileAsync = promisify(execFile);
25
+ // CAIP-2 chain identifiers for Solana networks.
26
+ // See OWS spec 07-supported-chains.md.
27
+ const SOLANA_CAIP2_MAINNET = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp';
28
+ const SOLANA_CAIP2_DEVNET = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG';
29
+ /** Alphanumeric, hyphens, and underscores — matches OWS wallet naming rules. */
30
+ const WALLET_NAME_RE = /^[a-zA-Z0-9_-]{1,64}$/;
31
+ // ── CLI helpers ──
32
+ /**
33
+ * True when the `ows` binary is reachable on PATH.
34
+ */
35
+ export async function isOwsInstalled() {
36
+ try {
37
+ await execFileAsync('ows', ['--version'], { timeout: 5_000 });
38
+ return true;
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ /**
45
+ * Validate wallet name format before passing to execFile.
46
+ * While execFile prevents shell injection, a garbage name produces
47
+ * confusing OWS CLI errors — better to catch it early.
48
+ */
49
+ function validateWalletName(name) {
50
+ if (!WALLET_NAME_RE.test(name)) {
51
+ throw new Error(`Invalid OWS wallet name "${name}". ` +
52
+ `Names must be 1-64 characters using letters, digits, hyphens, or underscores.`);
53
+ }
54
+ }
55
+ /**
56
+ * Return the Solana address for the named OWS wallet.
57
+ * Uses the current MCP session network (mainnet or devnet) to select
58
+ * the correct CAIP-2 chain key.
59
+ */
60
+ export async function getOwsSolanaAddress(walletName) {
61
+ validateWalletName(walletName);
62
+ const { stdout } = await execFileAsync('ows', ['wallet', 'info', '--wallet', walletName, '--json'], { timeout: 10_000 });
63
+ const info = JSON.parse(stdout);
64
+ // The CLI outputs accounts keyed by CAIP-2 chain id.
65
+ // Pick the key matching the active network.
66
+ const network = getNetwork();
67
+ const primaryKey = network === 'devnet' ? SOLANA_CAIP2_DEVNET : SOLANA_CAIP2_MAINNET;
68
+ const account = info.accounts?.[primaryKey] ??
69
+ info.accounts?.solana ??
70
+ // Fallback: scan for any key containing "solana"
71
+ Object.entries(info.accounts ?? {}).find(([k]) => k.includes('solana'))?.[1];
72
+ if (!account) {
73
+ throw new Error(`OWS wallet "${walletName}" has no Solana account. ` +
74
+ `Run \`ows wallet info --wallet ${walletName}\` to inspect.`);
75
+ }
76
+ // Account may be a string (address) or an object with an address field.
77
+ const addr = typeof account === 'string' ? account : account.address;
78
+ if (!addr) {
79
+ throw new Error(`Could not extract Solana address from OWS wallet "${walletName}".`);
80
+ }
81
+ return addr;
82
+ }
83
+ /**
84
+ * Sign raw message bytes via OWS. Returns the 64-byte Ed25519 signature.
85
+ * Assumes walletName was already validated by the caller (createOwsSigner).
86
+ */
87
+ async function owsSignBytes(walletName, messageHex) {
88
+ const { stdout } = await execFileAsync('ows', ['sign', 'message', '--wallet', walletName, '--chain', 'solana', '--message', messageHex, '--encoding', 'hex', '--json'], { timeout: 30_000 });
89
+ const result = JSON.parse(stdout);
90
+ const sigHex = result.signature;
91
+ if (!sigHex) {
92
+ throw new Error('OWS sign returned no signature');
93
+ }
94
+ return Uint8Array.from(Buffer.from(sigHex, 'hex'));
95
+ }
96
+ // ── Shared signer resolution ──
97
+ /**
98
+ * Resolve a signer from an OWS wallet name or the local Helius keypair.
99
+ * Returns a result-union so callers can return the MCP error directly.
100
+ *
101
+ * The returned `signer` is typed as `any` to avoid @solana/kit branded-type
102
+ * friction — it satisfies `isTransactionSigner()` at runtime.
103
+ *
104
+ * ⚠ If @solana/kit changes its signer interface (signTransactions /
105
+ * signMessages argument or return shapes), these casts will pass type-checking
106
+ * but fail at runtime. After any @solana/kit upgrade, verify the OWS signer
107
+ * still passes `isTransactionSigner()` and produces valid signatures in the
108
+ * full `signTransactionMessageWithSigners` pipeline.
109
+ */
110
+ export async function resolveOwsOrKeypairSigner(owsWallet) {
111
+ if (owsWallet) {
112
+ if (!WALLET_NAME_RE.test(owsWallet)) {
113
+ return { ok: false, error: mcpError(`Invalid OWS wallet name "${owsWallet}". Names must be 1-64 characters using letters, digits, hyphens, or underscores.`, { type: 'VALIDATION', code: 'INVALID_WALLET_NAME', retryable: false, recovery: 'Provide a valid wallet name (letters, digits, hyphens, underscores).' }) };
114
+ }
115
+ if (!await isOwsInstalled()) {
116
+ return { ok: false, error: mcpError('OWS CLI is not installed. Install it with `curl -fsSL https://docs.openwallet.sh/install.sh | bash` or `npm install -g @open-wallet-standard/core`.', { type: 'AUTH', code: 'OWS_NOT_INSTALLED', retryable: false, recovery: 'Install the OWS CLI, then retry.' }) };
117
+ }
118
+ try {
119
+ const signer = await createOwsSigner(owsWallet);
120
+ return { ok: true, signer, walletAddress: signer.address, owsWallet };
121
+ }
122
+ catch (err) {
123
+ return { ok: false, error: mcpError(`Failed to load OWS wallet "${owsWallet}": ${err.message}`, { type: 'AUTH', code: 'OWS_WALLET_ERROR', retryable: false, recovery: 'Run `ows wallet list` to see available wallets.' }) };
124
+ }
125
+ }
126
+ try {
127
+ const signerData = await loadSignerOrFail();
128
+ const signer = await createKeyPairSignerFromBytes(signerData.secretKey);
129
+ return { ok: true, signer, walletAddress: signerData.walletAddress };
130
+ }
131
+ catch {
132
+ return { ok: false, error: mcpError('No keypair found. Call `generateKeypair` first to create a wallet.', { type: 'AUTH', code: 'NO_KEYPAIR', retryable: false, recovery: 'Call `generateKeypair` to create a wallet.' }) };
133
+ }
134
+ }
135
+ export async function createOwsSigner(walletName) {
136
+ const solAddress = await getOwsSolanaAddress(walletName);
137
+ const addr = address(solAddress);
138
+ return {
139
+ address: addr,
140
+ async signTransactions(transactions) {
141
+ return Promise.all(transactions.map(async (tx) => {
142
+ const msgHex = Buffer.from(tx.messageBytes).toString('hex');
143
+ const sig = await owsSignBytes(walletName, msgHex);
144
+ return { [addr]: sig };
145
+ }));
146
+ },
147
+ async signMessages(messages) {
148
+ return Promise.all(messages.map(async (msg) => {
149
+ const hex = Buffer.from(msg.content).toString('hex');
150
+ const sig = await owsSignBytes(walletName, hex);
151
+ return { [addr]: sig };
152
+ }));
153
+ },
154
+ };
155
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "1.2.0";
1
+ export declare const version = "2.0.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = '1.2.0';
1
+ export const version = '2.0.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helius-mcp",
3
- "version": "1.2.0",
3
+ "version": "2.0.0",
4
4
  "description": "Official Helius MCP Server - Complete Solana blockchain data access for AI assistants",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -45,7 +45,7 @@
45
45
  "@solana-program/token": "^0.9.0",
46
46
  "@solana/kit": "^5.0.0",
47
47
  "bs58": "^6.0.0",
48
- "helius-sdk": "^2.2.2",
48
+ "helius-sdk": "^3.0.0",
49
49
  "zod": "^3.23.0"
50
50
  },
51
51
  "devDependencies": {