helius-mcp 1.3.0 → 2.1.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/CHANGELOG.md +151 -79
- package/LICENSE +21 -21
- package/README.md +144 -132
- package/dist/http.d.ts +1 -1
- package/dist/index.js +2 -56
- package/dist/results/store.d.ts +8 -0
- package/dist/results/store.js +72 -0
- package/dist/results/types.d.ts +47 -0
- package/dist/results/types.js +1 -0
- package/dist/router/action-groups.d.ts +6 -0
- package/dist/router/action-groups.js +32 -0
- package/dist/router/action-handlers.d.ts +20 -0
- package/dist/router/action-handlers.js +125 -0
- package/dist/router/actions.d.ts +12 -0
- package/dist/router/actions.js +125 -0
- package/dist/router/catalog.d.ts +6 -0
- package/dist/router/catalog.js +394 -0
- package/dist/router/context.d.ts +5 -0
- package/dist/router/context.js +10 -0
- package/dist/router/dispatch.d.ts +4 -0
- package/dist/router/dispatch.js +276 -0
- package/dist/router/instructions.d.ts +1 -0
- package/dist/router/instructions.js +25 -0
- package/dist/router/register.d.ts +2 -0
- package/dist/router/register.js +15 -0
- package/dist/router/required-params.d.ts +9 -0
- package/dist/router/required-params.js +68 -0
- package/dist/router/responses.d.ts +29 -0
- package/dist/router/responses.js +186 -0
- package/dist/router/schemas.d.ts +224 -0
- package/dist/router/schemas.js +204 -0
- package/dist/router/telemetry.d.ts +27 -0
- package/dist/router/telemetry.js +52 -0
- package/dist/router/types.d.ts +46 -0
- package/dist/router/types.js +1 -0
- package/dist/scripts/validate-catalog.d.ts +2 -2
- package/dist/scripts/validate-catalog.js +10 -10
- package/dist/tools/accounts.js +5 -5
- package/dist/tools/assets.js +5 -5
- package/dist/tools/auth.js +392 -319
- package/dist/tools/config.js +3 -3
- package/dist/tools/das-extras.js +6 -6
- package/dist/tools/docs.js +55 -41
- package/dist/tools/enhanced-websockets.js +13 -13
- package/dist/tools/fees.js +3 -3
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +2 -80
- package/dist/tools/laserstream.js +20 -23
- package/dist/tools/network.js +10 -4
- package/dist/tools/plans.d.ts +0 -5
- package/dist/tools/plans.js +167 -12
- package/dist/tools/product-catalog.d.ts +1 -0
- package/dist/tools/product-catalog.js +52 -17
- package/dist/tools/recommend.d.ts +0 -1
- package/dist/tools/recommend.js +9 -28
- package/dist/tools/shared.d.ts +1 -0
- package/dist/tools/shared.js +21 -13
- package/dist/tools/solana-knowledge.js +23 -7
- package/dist/tools/staking.d.ts +2 -0
- package/dist/tools/staking.js +268 -0
- package/dist/tools/transactions.js +256 -3
- package/dist/tools/transfers.js +38 -43
- package/dist/tools/wallet.js +77 -17
- package/dist/tools/webhooks.js +3 -3
- package/dist/tools/zk-compression.d.ts +2 -0
- package/dist/tools/zk-compression.js +781 -0
- package/dist/utils/config.d.ts +2 -2
- package/dist/utils/config.js +68 -6
- package/dist/utils/errors.d.ts +10 -1
- package/dist/utils/errors.js +46 -12
- package/dist/utils/feedback.js +1 -4
- package/dist/utils/helius.js +25 -14
- package/dist/utils/ows.d.ts +74 -0
- package/dist/utils/ows.js +155 -0
- package/dist/utils/resilience.d.ts +39 -0
- package/dist/utils/resilience.js +130 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +63 -64
- package/system-prompts/helius/claude.system.md +200 -170
- package/system-prompts/helius/full.md +3236 -2869
- package/system-prompts/helius/openai.developer.md +200 -170
- package/system-prompts/helius-dflow/claude.system.md +324 -290
- package/system-prompts/helius-dflow/full.md +4160 -3648
- package/system-prompts/helius-dflow/openai.developer.md +324 -290
- package/system-prompts/helius-jupiter/claude.system.md +333 -0
- package/system-prompts/helius-jupiter/full.md +5133 -0
- package/system-prompts/helius-jupiter/openai.developer.md +333 -0
- package/system-prompts/helius-okx/claude.system.md +182 -0
- package/system-prompts/helius-okx/full.md +584 -0
- package/system-prompts/helius-okx/openai.developer.md +182 -0
- package/system-prompts/helius-phantom/claude.system.md +345 -333
- package/system-prompts/helius-phantom/full.md +5649 -5473
- package/system-prompts/helius-phantom/openai.developer.md +345 -333
- package/system-prompts/svm/claude.system.md +159 -159
- package/system-prompts/svm/full.md +631 -631
- package/system-prompts/svm/openai.developer.md +159 -159
- package/dist/scripts/test-htmltotext.d.ts +0 -5
- package/dist/scripts/test-htmltotext.js +0 -67
- package/dist/scripts/test-solana-knowledge.d.ts +0 -9
- package/dist/scripts/test-solana-knowledge.js +0 -272
- package/dist/scripts/validate-templates.d.ts +0 -12
- package/dist/scripts/validate-templates.js +0 -94
package/dist/tools/auth.js
CHANGED
|
@@ -2,58 +2,57 @@ import { z } from 'zod';
|
|
|
2
2
|
import { generateKeypair } from 'helius-sdk/auth/generateKeypair';
|
|
3
3
|
import { loadKeypair } from 'helius-sdk/auth/loadKeypair';
|
|
4
4
|
import { getAddress } from 'helius-sdk/auth/getAddress';
|
|
5
|
-
import { checkSolBalance, checkUsdcBalance } from 'helius-sdk/auth/checkBalances';
|
|
6
|
-
import { agenticSignup } from 'helius-sdk/auth/agenticSignup';
|
|
7
|
-
import { getCheckoutPreview, executeCheckout, executeRenewal } from 'helius-sdk/auth/checkout';
|
|
8
5
|
import { listProjects } from 'helius-sdk/auth/listProjects';
|
|
9
6
|
import { getProject } from 'helius-sdk/auth/getProject';
|
|
10
|
-
import {
|
|
7
|
+
import { getCheckoutPreview } from 'helius-sdk/auth/checkout';
|
|
8
|
+
import { signup } from 'helius-sdk/auth/signup';
|
|
9
|
+
import { signupAndPay } from 'helius-sdk/auth/signupAndPay';
|
|
10
|
+
import { upgradePlan, upgradePlanAndPay } from 'helius-sdk/auth/upgradePlan';
|
|
11
|
+
import { purchaseCredits as sdkPurchaseCredits, purchaseCreditsAndPay, } from 'helius-sdk/auth/purchaseCredits';
|
|
12
|
+
import { payRenewal, payRenewalAndPay } from 'helius-sdk/auth/payRenewal';
|
|
11
13
|
import { MCP_USER_AGENT } from '../http.js';
|
|
12
|
-
import { setApiKey, hasApiKey, setSessionSecretKey, setSessionWalletAddress,
|
|
14
|
+
import { setApiKey, hasApiKey, setSessionSecretKey, setSessionWalletAddress, loadSignerOrFail, } from '../utils/helius.js';
|
|
13
15
|
import { mcpText, mcpError, handleToolError } from '../utils/errors.js';
|
|
14
16
|
import { fetchDoc, extractSections } from '../utils/docs.js';
|
|
15
17
|
import { sendFeedbackEvent, captureWalletAddress } from '../utils/feedback.js';
|
|
16
|
-
import { setSharedApiKey, setJwt, getJwt, SHARED_CONFIG_PATH, KEYPAIR_PATH, loadKeypairFromDisk, saveKeypairToDisk, keypairExistsOnDisk } from '../utils/config.js';
|
|
18
|
+
import { setSharedApiKey, setJwt, getJwt, SHARED_CONFIG_PATH, KEYPAIR_PATH, loadKeypairFromDisk, saveKeypairToDisk, keypairExistsOnDisk, } from '../utils/config.js';
|
|
17
19
|
import { HELIUS_PLANS } from './plans.js';
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
const renderPaymentLink = (paymentLink, flowName, resumeCmd) => `**${flowName}: payment required**\n\n` +
|
|
21
|
+
`Open this link in a browser to pay:\n\n` +
|
|
22
|
+
`\`${paymentLink.paymentUrl}\`\n\n` +
|
|
23
|
+
`Or send USDC + memo manually:\n` +
|
|
24
|
+
`- **Amount:** ${paymentLink.amountCents / 100} USDC\n` +
|
|
25
|
+
`- **Treasury:** \`${paymentLink.destinationWallet}\`\n` +
|
|
26
|
+
`- **Memo:** \`${paymentLink.memo}\`\n` +
|
|
27
|
+
`- **Plan:** ${paymentLink.planName}\n\n` +
|
|
28
|
+
`After paying, call \`${resumeCmd}\` to confirm activation locally.`;
|
|
22
29
|
export function registerAuthTools(server) {
|
|
23
|
-
// ── Getting Started
|
|
30
|
+
// ── Getting Started ──
|
|
24
31
|
server.tool('getStarted', 'Get setup instructions for Helius. Checks whether an API key is configured (not validated), whether a keypair exists on disk, and whether a JWT session is present, then tells you exactly what to do next. Call this when a user asks "how do I get started?" or needs onboarding help.', {}, async () => {
|
|
25
32
|
const lines = ['# Getting Started with Helius'];
|
|
26
33
|
const apiKeyConfigured = hasApiKey();
|
|
27
34
|
const hasKeypair = keypairExistsOnDisk();
|
|
28
35
|
const jwt = getJwt();
|
|
29
|
-
//
|
|
36
|
+
// Already fully set up
|
|
30
37
|
if (apiKeyConfigured && jwt) {
|
|
31
|
-
lines.push('', 'You\'re all set! Your API key and account session are configured.', '', '**What you can do:**', '- Query NFTs and tokens: `getAssetsByOwner`, `searchAssets`', '- Check balances: `getBalance`, `getTokenAccounts`', '- Parse transactions: `parseTransactions`', '- Manage webhooks: `createWebhook`, `getAllWebhooks`', '- Check your account: `getAccountStatus`', '', 'Just ask a question in plain English and the right tool will be used automatically.', '', '**IMPORTANT — if the user described a project they want to build, call `recommendStack` now
|
|
38
|
+
lines.push('', 'You\'re all set! Your API key and account session are configured.', '', '**What you can do:**', '- Query NFTs and tokens: `getAssetsByOwner`, `searchAssets`', '- Check balances: `getBalance`, `getTokenAccounts`', '- Parse transactions: `parseTransactions`', '- Manage webhooks: `createWebhook`, `getAllWebhooks`', '- Check your account: `getAccountStatus`', '', 'Just ask a question in plain English and the right tool will be used automatically.', '', '**IMPORTANT — if the user described a project they want to build, call `recommendStack` now.** It returns architecture recommendations with Helius products, MCP tools, credit costs, and reference files.');
|
|
32
39
|
return mcpText(lines.join('\n'));
|
|
33
40
|
}
|
|
34
|
-
//
|
|
41
|
+
// API key set but no JWT
|
|
35
42
|
if (apiKeyConfigured) {
|
|
36
|
-
lines.push('', 'Your API key is configured — all Helius tools are ready to use.', '', '
|
|
43
|
+
lines.push('', 'Your API key is configured — all Helius tools are ready to use.', '', 'To enable account-status info (plan, credits, rate limits), call `signup`. It will detect your existing account, no payment required.');
|
|
37
44
|
return mcpText(lines.join('\n'));
|
|
38
45
|
}
|
|
39
|
-
//
|
|
40
|
-
lines.push('', 'You need a Helius API key to use these tools.
|
|
41
|
-
// Adapt steps based on whether a keypair already exists
|
|
46
|
+
// No API key — need to set up
|
|
47
|
+
lines.push('', 'You need a Helius API key to use these tools. Three paths:', '', '## Path A — I already have an API key', '', 'Call `setHeliusApiKey` with your key from https://dashboard.helius.dev. All tools immediately available.', '', '## Path B — I have an existing dashboard.helius.dev account', '', 'If you already signed up at https://dashboard.helius.dev (email, Google, GitHub, or SSO), run `helius login` in your terminal:', '', '```bash', 'npx helius-cli@latest login', '```', '', 'This opens your default browser to authenticate via OAuth/PKCE and writes a session token to `~/.helius/config.json`. This MCP picks it up automatically — no further setup. Then call `setHeliusApiKey` with one of your keys (visible via `helius apikeys`), or just call `getAccountStatus` to confirm the session.', '', '## Path C — Create a new account via crypto checkout', '', '1. Call `generateKeypair` to create (or load) a Solana wallet — required for every signup mode, including link mode (the wallet address is bound to the payment intent).', '2. Call `signup` with your email/name. By default (`mode: "link"`) it returns a hosted-checkout URL the user can open in a browser to pay with any wallet.', '3. After the user pays, call `signup` again with `mode: "resume"` to poll the intent and provision the API key.', '', 'For an autopay flow that pays USDC directly from the local keypair, pass `mode: "autopay"` to `signup`. The wallet must hold ~0.001 SOL for fees and the plan amount in USDC.', '');
|
|
42
48
|
if (hasKeypair) {
|
|
43
|
-
lines.push(
|
|
49
|
+
lines.push(`_(A keypair already exists at \`${KEYPAIR_PATH}\` — \`signup\` will reuse it; you can skip step 1.)_`, '');
|
|
44
50
|
}
|
|
45
|
-
else {
|
|
46
|
-
lines.push('### Step 1: Generate a keypair', 'Call the `generateKeypair` tool. It creates a Solana wallet and returns the address.', '');
|
|
47
|
-
}
|
|
48
|
-
lines.push('### Step 2: Fund the wallet', 'Send the following to the wallet address from Step 1:', '- **~0.001 SOL** — covers transaction fees', '- **1 USDC** — pays for the basic plan ($1)', '', 'You can send from any Solana wallet, exchange, or on-ramp.', 'The USDC token mint on Solana is: `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`', '', `For paid plans, send more USDC instead: ${PAID_PLAN_ORDER.filter(k => k in PLAN_CATALOG).map(k => `$${PLAN_CATALOG[k].monthlyPrice / 100} (${PLAN_CATALOG[k].name})`).join(', ')}.`, '', '### Step 3: Verify funding', 'Call `checkSignupBalance` to confirm your SOL and USDC balances are sufficient.', '', '### Step 4: Create the account', 'Call `agenticSignup` to process the payment and create your Helius account.', 'Your API key will be configured automatically — no extra steps needed.', '', '> **Paid plans only:** `agenticSignup` requires `email`, `firstName`, and `lastName` for developer/business/professional plans. Basic plan ($1) does not require them.', '', '---', '', '## Path C — Use the Helius CLI', '', 'Same flow from the terminal:', '```', 'npx helius-cli@latest keygen # Generate keypair', '# Fund the wallet address shown above', 'npx helius-cli@latest signup # Verify balance + create account', '```', '', '---', '', '**After setup:** Use `recommendStack` to plan your project — describe what you\'re building and get architecture recommendations at different cost and complexity levels.');
|
|
49
51
|
return mcpText(lines.join('\n'));
|
|
50
52
|
});
|
|
51
53
|
// ── Keypair Generation ──
|
|
52
|
-
server.tool('generateKeypair', 'Generate a new Solana keypair
|
|
54
|
+
server.tool('generateKeypair', 'Generate a new Solana keypair (or load the existing one from disk). Returns the wallet address. Required before any `signup` call — the wallet address is bound to the payment intent in both link and autopay modes.', {}, async () => {
|
|
53
55
|
try {
|
|
54
|
-
// Reset balance-check counter for fresh signup flow
|
|
55
|
-
insufficientBalanceChecks = 0;
|
|
56
|
-
// Check disk first — reuse existing keypair if available
|
|
57
56
|
const existingKey = loadKeypairFromDisk();
|
|
58
57
|
if (existingKey) {
|
|
59
58
|
const walletKeypair = loadKeypair(existingKey);
|
|
@@ -62,13 +61,8 @@ export function registerAuthTools(server) {
|
|
|
62
61
|
setSessionWalletAddress(address);
|
|
63
62
|
captureWalletAddress(address);
|
|
64
63
|
return mcpText(`**Existing Keypair Loaded** from \`${KEYPAIR_PATH}\`\n\n` +
|
|
65
|
-
`**Wallet Address:** \`${address}
|
|
66
|
-
`To create a Helius account, fund this wallet with:\n` +
|
|
67
|
-
`- **~0.001 SOL** for transaction fees\n` +
|
|
68
|
-
`- **1 USDC** for basic plan (or more for paid plans)\n\n` +
|
|
69
|
-
`Then call \`agenticSignup\` to complete account creation.`);
|
|
64
|
+
`**Wallet Address:** \`${address}\``);
|
|
70
65
|
}
|
|
71
|
-
// Generate new keypair and persist to disk
|
|
72
66
|
const keypair = await generateKeypair();
|
|
73
67
|
const walletKeypair = loadKeypair(keypair.secretKey);
|
|
74
68
|
const address = await getAddress(walletKeypair);
|
|
@@ -78,95 +72,33 @@ export function registerAuthTools(server) {
|
|
|
78
72
|
captureWalletAddress(address);
|
|
79
73
|
return mcpText(`**Keypair Generated**\n\n` +
|
|
80
74
|
`**Wallet Address:** \`${address}\`\n` +
|
|
81
|
-
`**Saved to:** \`${KEYPAIR_PATH}
|
|
82
|
-
`To create a Helius account, fund this wallet with:\n` +
|
|
83
|
-
`- **~0.001 SOL** for transaction fees\n` +
|
|
84
|
-
`- **1 USDC** for basic plan (or more for paid plans)\n\n` +
|
|
85
|
-
`Then call \`agenticSignup\` to complete account creation.`);
|
|
75
|
+
`**Saved to:** \`${KEYPAIR_PATH}\``);
|
|
86
76
|
}
|
|
87
77
|
catch (err) {
|
|
88
78
|
return handleToolError(err, 'Error generating keypair');
|
|
89
79
|
}
|
|
90
80
|
});
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const usdcOk = usdcBalance >= 1000000n; // 1 USDC for basic plan
|
|
113
|
-
const funded = solOk && usdcOk;
|
|
114
|
-
// Reset counter when balance is sufficient
|
|
115
|
-
if (funded) {
|
|
116
|
-
insufficientBalanceChecks = 0;
|
|
117
|
-
return mcpText(`**Signup Wallet Balance** (\`${address}\`)\n\n` +
|
|
118
|
-
`- **SOL:** ${solAmount.toFixed(6)} (sufficient)\n` +
|
|
119
|
-
`- **USDC:** ${usdcAmount.toFixed(2)} (sufficient for basic)\n\n` +
|
|
120
|
-
`**Status:** Ready for signup (basic plan). For paid plans, ensure sufficient USDC for the plan price.\n\n` +
|
|
121
|
-
`Call \`agenticSignup\` to proceed.`);
|
|
122
|
-
}
|
|
123
|
-
// Insufficient — increment counter and escalate guidance
|
|
124
|
-
insufficientBalanceChecks++;
|
|
125
|
-
const missing = [];
|
|
126
|
-
if (!solOk)
|
|
127
|
-
missing.push(`~0.001 SOL (have ${solAmount.toFixed(6)})`);
|
|
128
|
-
if (!usdcOk)
|
|
129
|
-
missing.push(`1 USDC (have ${usdcAmount.toFixed(2)})`);
|
|
130
|
-
let balanceBlock = `**Signup Wallet Balance** (\`${address}\`)\n\n` +
|
|
131
|
-
`- **SOL:** ${solAmount.toFixed(6)} ${solOk ? '(sufficient)' : '(insufficient)'}\n` +
|
|
132
|
-
`- **USDC:** ${usdcAmount.toFixed(2)} ${usdcOk ? '(sufficient for basic)' : '(insufficient)'}\n\n` +
|
|
133
|
-
`**Status:** Need more funds: ${missing.join(', ')}`;
|
|
134
|
-
if (insufficientBalanceChecks === 1) {
|
|
135
|
-
// First check — normal guidance
|
|
136
|
-
balanceBlock +=
|
|
137
|
-
`\n\n**Action required:** Ask the user to send the missing funds to \`${address}\`. ` +
|
|
138
|
-
`Do **not** call \`checkSignupBalance\` again until the user confirms they have sent the funds.`;
|
|
139
|
-
}
|
|
140
|
-
else if (insufficientBalanceChecks < MAX_BALANCE_CHECKS_BEFORE_STOP) {
|
|
141
|
-
// Second check — firmer nudge
|
|
142
|
-
balanceBlock +=
|
|
143
|
-
`\n\n**⚠ Balance still insufficient (check ${insufficientBalanceChecks}/${MAX_BALANCE_CHECKS_BEFORE_STOP}).** ` +
|
|
144
|
-
`The wallet has not been funded yet. Ask the user to confirm they have sent funds to \`${address}\` before calling this tool again.`;
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
// Third+ check — hard stop
|
|
148
|
-
balanceBlock +=
|
|
149
|
-
`\n\n**🛑 Balance checked ${insufficientBalanceChecks} times — still insufficient. Stop polling.** ` +
|
|
150
|
-
`The wallet \`${address}\` has not received funds. ` +
|
|
151
|
-
`Tell the user the exact amounts needed and the wallet address, then **wait for the user to explicitly confirm** they have sent funds before calling \`checkSignupBalance\` again. ` +
|
|
152
|
-
`Do not retry automatically.`;
|
|
153
|
-
}
|
|
154
|
-
return mcpText(balanceBlock);
|
|
155
|
-
}
|
|
156
|
-
catch (err) {
|
|
157
|
-
return handleToolError(err, 'Error checking balances');
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
server.tool('agenticSignup', 'Create a Helius account using the generated keypair. Default: basic plan ($1 USDC). For paid plans (developer/business/professional), email, firstName, and lastName are required. On success, automatically configures the API key for this session.', {
|
|
161
|
-
plan: z.string().optional().describe('Plan to sign up for: "basic" ($1, default), "developer", "business", or "professional"'),
|
|
162
|
-
period: z.enum(["monthly", "yearly"]).optional().describe('Billing period for paid plans (default: monthly)'),
|
|
163
|
-
email: z.string().email().optional().describe('Email address (required for paid plans)'),
|
|
164
|
-
firstName: z.string().optional().describe('First name (required for paid plans)'),
|
|
165
|
-
lastName: z.string().optional().describe('Last name (required for paid plans)'),
|
|
166
|
-
couponCode: z.string().optional().describe('Coupon code for paid plans'),
|
|
167
|
-
discoveryPath: z.string().optional().describe('How did you discover Helius? Helps us improve agent onboarding'),
|
|
168
|
-
frictionPoints: z.string().optional().describe('What friction did you hit finding or setting up Helius? Free-form feedback'),
|
|
169
|
-
}, async ({ plan, period, email, firstName, lastName, couponCode, discoveryPath, frictionPoints }) => {
|
|
81
|
+
// ── Signup ──
|
|
82
|
+
server.tool('signup', 'Create a Helius account via crypto checkout. Requires `generateKeypair` to have been called first (the wallet address is bound to the payment intent in every mode). Default `mode: "link"` returns a hosted-checkout URL the user opens in a browser to pay USDC. `mode: "autopay"` sends USDC + memo directly from the local keypair. `mode: "resume"` polls a pending payment intent and provisions the API key after the user has paid in a browser. NOTE: resume strictly polls the JWT-bound project list and cannot distinguish "pending signup awaiting browser pay" from "stale JWT left over from a long-completed signup" — if the wallet already has an API key, prefer `setHeliusApiKey` over re-resuming.', {
|
|
83
|
+
mode: z
|
|
84
|
+
.enum(['link', 'autopay', 'resume'])
|
|
85
|
+
.default('link')
|
|
86
|
+
.describe('link (default): print payment URL and exit. autopay: send USDC + memo from local keypair. resume: poll an in-flight payment intent.'),
|
|
87
|
+
plan: z
|
|
88
|
+
.enum(['agent', 'developer', 'business', 'professional'])
|
|
89
|
+
.default('agent')
|
|
90
|
+
.describe('Plan: agent ($10 one-time, default), developer, business, professional'),
|
|
91
|
+
period: z
|
|
92
|
+
.enum(['monthly', 'yearly'])
|
|
93
|
+
.optional()
|
|
94
|
+
.describe('Billing period for subscription plans (default: monthly). Ignored for agent.'),
|
|
95
|
+
email: z.string().email().optional().describe('Email (required for fresh signup; not needed for resume)'),
|
|
96
|
+
firstName: z.string().optional().describe('First name (required for fresh signup)'),
|
|
97
|
+
lastName: z.string().optional().describe('Last name (required for fresh signup)'),
|
|
98
|
+
couponCode: z.string().optional().describe('Coupon code'),
|
|
99
|
+
discoveryPath: z.string().optional().describe('How did you discover Helius?'),
|
|
100
|
+
frictionPoints: z.string().optional().describe('What friction did you hit?'),
|
|
101
|
+
}, async ({ mode, plan, period, email, firstName, lastName, couponCode, discoveryPath, frictionPoints }) => {
|
|
170
102
|
if (discoveryPath || frictionPoints) {
|
|
171
103
|
sendFeedbackEvent({
|
|
172
104
|
type: 'discovery',
|
|
@@ -175,69 +107,135 @@ export function registerAuthTools(server) {
|
|
|
175
107
|
});
|
|
176
108
|
}
|
|
177
109
|
try {
|
|
110
|
+
// Resume mode: just poll the existing JWT/intent. Caller must have
|
|
111
|
+
// already gone through link mode in a previous invocation; the JWT
|
|
112
|
+
// from that invocation is on disk.
|
|
113
|
+
if (mode === 'resume') {
|
|
114
|
+
return await handleResumeSignup();
|
|
115
|
+
}
|
|
178
116
|
let signerData;
|
|
179
117
|
try {
|
|
180
118
|
signerData = await loadSignerOrFail();
|
|
181
119
|
}
|
|
182
120
|
catch {
|
|
183
|
-
return mcpError('No signup keypair found. Call `generateKeypair` first to create a wallet,
|
|
184
|
-
}
|
|
185
|
-
|
|
121
|
+
return mcpError('No signup keypair found. Call `generateKeypair` first to create a wallet, then retry.', { type: 'AUTH', code: 'NO_KEYPAIR', retryable: false, recovery: 'Call `generateKeypair`.' });
|
|
122
|
+
}
|
|
123
|
+
if (mode === 'autopay') {
|
|
124
|
+
const result = await signupAndPay({
|
|
125
|
+
secretKey: signerData.secretKey,
|
|
126
|
+
plan: plan,
|
|
127
|
+
period,
|
|
128
|
+
email,
|
|
129
|
+
firstName,
|
|
130
|
+
lastName,
|
|
131
|
+
couponCode,
|
|
132
|
+
});
|
|
133
|
+
return renderSignupAndPayResult(result);
|
|
134
|
+
}
|
|
135
|
+
// mode === "link" (default)
|
|
136
|
+
const result = await signup({
|
|
186
137
|
secretKey: signerData.secretKey,
|
|
187
|
-
|
|
188
|
-
plan,
|
|
138
|
+
plan: plan,
|
|
189
139
|
period,
|
|
190
140
|
email,
|
|
191
141
|
firstName,
|
|
192
142
|
lastName,
|
|
193
143
|
couponCode,
|
|
194
144
|
});
|
|
195
|
-
|
|
196
|
-
if (result.apiKey) {
|
|
145
|
+
if (result.kind === 'already_subscribed') {
|
|
197
146
|
setApiKey(result.apiKey);
|
|
198
147
|
setSharedApiKey(result.apiKey);
|
|
199
|
-
}
|
|
200
|
-
// Persist JWT to disk
|
|
201
|
-
if (result.jwt) {
|
|
202
148
|
setJwt(result.jwt);
|
|
203
|
-
}
|
|
204
|
-
const saveNote = result.apiKey
|
|
205
|
-
? `\nAPI key configured for this session and saved to \`${SHARED_CONFIG_PATH}\`. All Helius tools are now ready to use.`
|
|
206
|
-
: '';
|
|
207
|
-
if (result.status === 'existing_project') {
|
|
208
149
|
return mcpText(`**Helius Account Found**\n\n` +
|
|
209
|
-
`You already have a Helius account. No payment was needed.\n\n` +
|
|
210
|
-
`- **Wallet:** \`${result.walletAddress}\`\n` +
|
|
150
|
+
`You already have a Helius account on this plan. No payment was needed.\n\n` +
|
|
211
151
|
`- **Project ID:** \`${result.projectId}\`\n` +
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
152
|
+
`- **API Key:** \`${result.apiKey}\`\n` +
|
|
153
|
+
`- **Mainnet RPC:** \`${result.endpoints.mainnet}\`\n` +
|
|
154
|
+
`- **Devnet RPC:** \`${result.endpoints.devnet}\`\n\n` +
|
|
155
|
+
`API key configured for this session and saved to \`${SHARED_CONFIG_PATH}\`.`);
|
|
156
|
+
}
|
|
157
|
+
if (result.kind === 'upgrade_required') {
|
|
158
|
+
return mcpText(`**Plan change required**\n\n` +
|
|
159
|
+
`Your wallet is already on plan \`${result.currentPlan}\`. ` +
|
|
160
|
+
`Use the \`upgradePlan\` tool to switch to \`${result.requestedPlan}\`.`);
|
|
161
|
+
}
|
|
162
|
+
// payment_required — store JWT so resume mode can use it.
|
|
163
|
+
setJwt(result.jwt);
|
|
164
|
+
return mcpText(renderPaymentLink(result.paymentLink, 'Sign up', 'signup with mode: "resume"'));
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
return handleToolError(err, 'Error during signup');
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
// ── Account Status ──
|
|
171
|
+
server.tool('getAccountStatus', 'Check your Helius account status: current plan, remaining credits, rate limits, and billing cycle. Requires a JWT session (i.e., you signed up via `signup`). If you only have an API key configured, auth status is confirmed but credit data is unavailable — call `signup` to enable full status.', {}, async () => {
|
|
172
|
+
try {
|
|
173
|
+
if (!hasApiKey()) {
|
|
174
|
+
return mcpText(`## Account Status\n\n` +
|
|
175
|
+
`**Auth:** Not authenticated\n\n` +
|
|
176
|
+
`No API key or session found. To get started:\n` +
|
|
177
|
+
`- If you have a key: use the \`setHeliusApiKey\` tool\n` +
|
|
178
|
+
`- If you need an account: use \`signup\``);
|
|
217
179
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
saveNote);
|
|
180
|
+
const jwt = getJwt();
|
|
181
|
+
if (!jwt) {
|
|
182
|
+
return mcpText(`## Account Status\n\n` +
|
|
183
|
+
`**Auth:** Authenticated (API key configured)\n` +
|
|
184
|
+
`**Credit usage:** Not available — no JWT session found\n\n` +
|
|
185
|
+
`To see your plan, rate limits, and credit balance, call \`signup\`. Your existing account will be detected automatically — no payment needed.`);
|
|
225
186
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
187
|
+
const projects = await listProjects(jwt, MCP_USER_AGENT);
|
|
188
|
+
if (projects.length === 0) {
|
|
189
|
+
return mcpError('No projects found. Call `signup` to create an account first.', { type: 'AUTH', code: 'NO_PROJECT', retryable: false, recovery: 'Call `signup`.' });
|
|
190
|
+
}
|
|
191
|
+
const projectId = projects[0].id;
|
|
192
|
+
const details = await getProject(jwt, projectId, MCP_USER_AGENT);
|
|
193
|
+
const planKey = details.subscriptionPlanDetails?.currentPlan ?? 'unknown';
|
|
194
|
+
const upcomingPlan = details.subscriptionPlanDetails?.upcomingPlan;
|
|
195
|
+
const isUpgrading = details.subscriptionPlanDetails?.isUpgrading ?? false;
|
|
196
|
+
const planInfo = HELIUS_PLANS[planKey];
|
|
197
|
+
const usage = details.creditsUsage;
|
|
198
|
+
const cycle = details.billingCycle;
|
|
199
|
+
const lines = [`## Account Status`, ''];
|
|
200
|
+
lines.push(`**Auth:** Authenticated`);
|
|
201
|
+
lines.push(`**Plan:** ${planInfo ? planInfo.name : planKey} | **Project:** \`${projectId}\``);
|
|
202
|
+
if (isUpgrading && upcomingPlan && upcomingPlan !== planKey) {
|
|
203
|
+
lines.push(`**Upcoming plan:** ${upcomingPlan} (takes effect at next billing cycle)`);
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const billingDoc = await fetchDoc('billing');
|
|
207
|
+
const rateLimits = extractSections(billingDoc, ['standard rate limits', 'rate limits'], {
|
|
208
|
+
includeLooseMatches: false,
|
|
209
|
+
});
|
|
210
|
+
if (rateLimits) {
|
|
211
|
+
lines.push('', '### Rate Limits (live)', '', rateLimits);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// best-effort
|
|
216
|
+
}
|
|
217
|
+
if (usage) {
|
|
218
|
+
const total = usage.remainingCredits + usage.totalCreditsUsed;
|
|
219
|
+
const pctUsed = total > 0 ? ((usage.totalCreditsUsed / total) * 100).toFixed(1) : '0.0';
|
|
220
|
+
const pctRemaining = total > 0 ? (100 - parseFloat(pctUsed)).toFixed(1) : '100.0';
|
|
221
|
+
let cycleStr = '';
|
|
222
|
+
if (cycle) {
|
|
223
|
+
const end = new Date(cycle.end);
|
|
224
|
+
const now = new Date();
|
|
225
|
+
const daysLeft = Math.ceil((end.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
|
226
|
+
cycleStr = ` (${cycle.start} - ${cycle.end}, ${daysLeft} day${daysLeft !== 1 ? 's' : ''} remaining)`;
|
|
227
|
+
}
|
|
228
|
+
lines.push('', `### Credits — Billing Cycle${cycleStr}`);
|
|
229
|
+
lines.push(`- **Remaining:** ${usage.remainingCredits.toLocaleString()} / ${total.toLocaleString()} (${pctRemaining}%)`);
|
|
230
|
+
lines.push(`- **Used:** ${usage.totalCreditsUsed.toLocaleString()} (${pctUsed}%)`);
|
|
231
|
+
}
|
|
232
|
+
return mcpText(lines.join('\n'));
|
|
235
233
|
}
|
|
236
234
|
catch (err) {
|
|
237
|
-
return handleToolError(err, 'Error
|
|
235
|
+
return handleToolError(err, 'Error fetching account status');
|
|
238
236
|
}
|
|
239
237
|
});
|
|
240
|
-
// ── Upgrade
|
|
238
|
+
// ── Upgrade ──
|
|
241
239
|
server.tool('previewUpgrade', 'Preview pricing for a plan upgrade with proration details. Shows current plan, new plan cost, prorated credits, and amount due today.', {
|
|
242
240
|
plan: z.enum(['developer', 'business', 'professional']).describe('Target plan name'),
|
|
243
241
|
period: z.enum(['monthly', 'yearly']).default('monthly').describe('Billing period'),
|
|
@@ -246,11 +244,21 @@ export function registerAuthTools(server) {
|
|
|
246
244
|
try {
|
|
247
245
|
const jwt = getJwt();
|
|
248
246
|
if (!jwt) {
|
|
249
|
-
return mcpError('Not authenticated. Call `
|
|
247
|
+
return mcpError('Not authenticated. Call `signup`, or run `helius login` in your terminal if you have a dashboard.helius.dev account.', {
|
|
248
|
+
type: 'AUTH',
|
|
249
|
+
code: 'NOT_AUTHENTICATED',
|
|
250
|
+
retryable: false,
|
|
251
|
+
recovery: 'Call `signup`, or run `helius login` in your terminal if you already have a dashboard.helius.dev account.',
|
|
252
|
+
});
|
|
250
253
|
}
|
|
251
254
|
const projects = await listProjects(jwt, MCP_USER_AGENT);
|
|
252
255
|
if (projects.length === 0) {
|
|
253
|
-
return mcpError('No projects found. Call `
|
|
256
|
+
return mcpError('No projects found. Call `signup` to create an account first.', {
|
|
257
|
+
type: 'AUTH',
|
|
258
|
+
code: 'NO_PROJECT',
|
|
259
|
+
retryable: false,
|
|
260
|
+
recovery: 'Call `signup`.',
|
|
261
|
+
});
|
|
254
262
|
}
|
|
255
263
|
const projectId = projects[0].id;
|
|
256
264
|
const projectDetails = await getProject(jwt, projectId, MCP_USER_AGENT);
|
|
@@ -273,13 +281,7 @@ export function registerAuthTools(server) {
|
|
|
273
281
|
if (preview.coupon?.valid) {
|
|
274
282
|
text += `- Coupon (${preview.coupon.code}): ${preview.coupon.description || 'Applied'}\n`;
|
|
275
283
|
}
|
|
276
|
-
else if (preview.coupon && !preview.coupon.valid) {
|
|
277
|
-
text += `- Coupon (${preview.coupon.code}): **Invalid** — ${preview.coupon.invalidReason || 'not applicable'}\n`;
|
|
278
|
-
}
|
|
279
284
|
text += `\n**Due Today: $${(preview.dueToday / 100).toFixed(2)}**\n`;
|
|
280
|
-
if (preview.note) {
|
|
281
|
-
text += `\n_${preview.note}_\n`;
|
|
282
|
-
}
|
|
283
285
|
text += `\nTo proceed, use the \`upgradePlan\` tool with plan: '${plan}'.`;
|
|
284
286
|
return mcpText(text);
|
|
285
287
|
}
|
|
@@ -287,204 +289,275 @@ export function registerAuthTools(server) {
|
|
|
287
289
|
return handleToolError(err, 'Error previewing upgrade');
|
|
288
290
|
}
|
|
289
291
|
});
|
|
290
|
-
server.tool('upgradePlan', 'Upgrade your Helius plan.
|
|
292
|
+
server.tool('upgradePlan', 'Upgrade your Helius plan via crypto checkout. Default `mode: "link"` returns a hosted-checkout URL. `mode: "autopay"` pays USDC from the local keypair and polls. Contact info (email/firstName/lastName) is only needed for first-time upgrades — backend auto-fetches from existing customer otherwise.', {
|
|
293
|
+
mode: z.enum(['link', 'autopay']).default('link').describe('link (default) or autopay'),
|
|
291
294
|
plan: z.enum(['developer', 'business', 'professional']).describe('Target plan name'),
|
|
292
295
|
period: z.enum(['monthly', 'yearly']).default('monthly').describe('Billing period'),
|
|
293
296
|
couponCode: z.string().optional().describe('Optional coupon code'),
|
|
294
|
-
email: z.string().email().optional().describe('Email
|
|
295
|
-
firstName: z.string().optional().describe('First name (
|
|
296
|
-
lastName: z.string().optional().describe('Last name (
|
|
297
|
-
}, async ({ plan, period, couponCode, email, firstName, lastName }) => {
|
|
297
|
+
email: z.string().email().optional().describe('Email (only for first-time upgrades)'),
|
|
298
|
+
firstName: z.string().optional().describe('First name (only for first-time upgrades)'),
|
|
299
|
+
lastName: z.string().optional().describe('Last name (only for first-time upgrades)'),
|
|
300
|
+
}, async ({ mode, plan, period, couponCode, email, firstName, lastName }) => {
|
|
298
301
|
try {
|
|
299
|
-
// All-or-none customer info validation
|
|
300
|
-
const hasAny = email || firstName || lastName;
|
|
301
|
-
if (hasAny && (!email || !firstName || !lastName)) {
|
|
302
|
-
const missing = [
|
|
303
|
-
!email && 'email',
|
|
304
|
-
!firstName && 'firstName',
|
|
305
|
-
!lastName && 'lastName',
|
|
306
|
-
].filter(Boolean);
|
|
307
|
-
return mcpError(`Partial customer info provided. If any of email/firstName/lastName is given, all three are required. Missing: ${missing.join(', ')}`);
|
|
308
|
-
}
|
|
309
|
-
let signerData;
|
|
310
|
-
try {
|
|
311
|
-
signerData = await loadSignerOrFail();
|
|
312
|
-
}
|
|
313
|
-
catch {
|
|
314
|
-
return mcpError('No keypair found. Call `generateKeypair` first.');
|
|
315
|
-
}
|
|
316
302
|
const jwt = getJwt();
|
|
317
303
|
if (!jwt) {
|
|
318
|
-
return mcpError('Not authenticated. Call `
|
|
304
|
+
return mcpError('Not authenticated. Call `signup`, or run `helius login` in your terminal if you have a dashboard.helius.dev account.', {
|
|
305
|
+
type: 'AUTH',
|
|
306
|
+
code: 'NOT_AUTHENTICATED',
|
|
307
|
+
retryable: false,
|
|
308
|
+
recovery: 'Call `signup`, or run `helius login` in your terminal if you already have a dashboard.helius.dev account.',
|
|
309
|
+
});
|
|
319
310
|
}
|
|
320
311
|
const projects = await listProjects(jwt, MCP_USER_AGENT);
|
|
321
312
|
if (projects.length === 0) {
|
|
322
|
-
return mcpError('No projects found. Call `
|
|
313
|
+
return mcpError('No projects found. Call `signup` first.', {
|
|
314
|
+
type: 'AUTH',
|
|
315
|
+
code: 'NO_PROJECT',
|
|
316
|
+
retryable: false,
|
|
317
|
+
recovery: 'Call `signup`.',
|
|
318
|
+
});
|
|
323
319
|
}
|
|
324
320
|
const projectId = projects[0].id;
|
|
325
|
-
|
|
321
|
+
if (mode === 'autopay') {
|
|
322
|
+
let signerData;
|
|
323
|
+
try {
|
|
324
|
+
signerData = await loadSignerOrFail();
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
return mcpError('No keypair. Autopay needs one — call `generateKeypair` first.', {
|
|
328
|
+
type: 'AUTH',
|
|
329
|
+
code: 'NO_KEYPAIR',
|
|
330
|
+
retryable: false,
|
|
331
|
+
recovery: 'Call `generateKeypair`.',
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
const result = await upgradePlanAndPay({
|
|
335
|
+
secretKey: signerData.secretKey,
|
|
336
|
+
jwt,
|
|
337
|
+
projectId,
|
|
338
|
+
plan,
|
|
339
|
+
period,
|
|
340
|
+
couponCode,
|
|
341
|
+
email,
|
|
342
|
+
firstName,
|
|
343
|
+
lastName,
|
|
344
|
+
});
|
|
345
|
+
return renderUpgradeOrCreditsAndPayResult(result, 'Upgrade');
|
|
346
|
+
}
|
|
347
|
+
// mode === "link"
|
|
348
|
+
const result = await upgradePlan({
|
|
349
|
+
jwt,
|
|
350
|
+
projectId,
|
|
326
351
|
plan,
|
|
327
352
|
period,
|
|
328
|
-
refId: projectId,
|
|
329
353
|
couponCode,
|
|
330
354
|
email,
|
|
331
355
|
firstName,
|
|
332
356
|
lastName,
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return mcpError(`**Upgrade ${result.status}**\n\n` +
|
|
336
|
-
(result.error ? `Error: ${result.error}\n` : '') +
|
|
337
|
-
(result.txSignature ? `TX: \`${result.txSignature}\`\n` : '') +
|
|
338
|
-
`\nIf you need help, contact support with the payment intent ID: \`${result.paymentIntentId}\``);
|
|
339
|
-
}
|
|
340
|
-
const planInfo = PLAN_CATALOG[plan];
|
|
341
|
-
return mcpText(`**Plan Upgraded Successfully**\n\n` +
|
|
342
|
-
`- **New Plan:** ${planInfo.name} (${period})\n` +
|
|
343
|
-
`- **Project ID:** \`${projectId}\`\n` +
|
|
344
|
-
(result.txSignature ? `- **Payment TX:** \`${result.txSignature}\`\n` : '') +
|
|
345
|
-
`\nYour new plan is now active with ${(planInfo.credits / 1_000_000).toFixed(0)}M credits and ${planInfo.requestsPerSecond} RPS.`);
|
|
357
|
+
});
|
|
358
|
+
return mcpText(renderPaymentLink(result.paymentLink, 'Upgrade', 'getAccountStatus'));
|
|
346
359
|
}
|
|
347
360
|
catch (err) {
|
|
348
361
|
return handleToolError(err, 'Error upgrading plan');
|
|
349
362
|
}
|
|
350
363
|
});
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
'
|
|
354
|
-
'
|
|
364
|
+
// ── Prepaid Credits ──
|
|
365
|
+
server.tool('purchaseCredits', 'Buy prepaid credits as a one-time USDC top-up. AGENT PLAN ONLY: each unit of `qty` is 1,000,000 credits at $10 USDC, no recurring sub. Subscription plans (Developer / Business / Professional) get monthly allotments and overage is auto-billed at $5/M on the next invoice — they can NOT use this tool; this tool will reject the call with UNSUPPORTED_PLAN. Default `mode: "link"` returns a hosted-checkout URL; `mode: "autopay"` pays USDC from the local keypair and polls.', {
|
|
366
|
+
mode: z.enum(['link', 'autopay']).default('link').describe('link (default) or autopay'),
|
|
367
|
+
qty: z.number().int().min(1).default(1).describe('Quantity multiplier (each unit = 1M credits at $10 USDC)'),
|
|
368
|
+
couponCode: z.string().optional().describe('Optional coupon code'),
|
|
369
|
+
}, async ({ mode, qty, couponCode }) => {
|
|
355
370
|
try {
|
|
356
|
-
// ── Tier 1: not authenticated at all ──
|
|
357
|
-
if (!hasApiKey()) {
|
|
358
|
-
return mcpText(`## Account Status\n\n` +
|
|
359
|
-
`**Auth:** Not authenticated\n\n` +
|
|
360
|
-
`No API key or session found. To get started:\n` +
|
|
361
|
-
`- If you have a key: use the \`setHeliusApiKey\` tool\n` +
|
|
362
|
-
`- If you need an account: use \`generateKeypair\` → fund wallet → \`agenticSignup\``);
|
|
363
|
-
}
|
|
364
|
-
// ── Tier 2: API key present but no JWT — can't reach dashboard API ──
|
|
365
371
|
const jwt = getJwt();
|
|
366
372
|
if (!jwt) {
|
|
367
|
-
return
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
`
|
|
373
|
+
return mcpError('Not authenticated. Call `signup`, or run `helius login` in your terminal if you have a dashboard.helius.dev account.', {
|
|
374
|
+
type: 'AUTH',
|
|
375
|
+
code: 'NOT_AUTHENTICATED',
|
|
376
|
+
retryable: false,
|
|
377
|
+
recovery: 'Call `signup`, or run `helius login` in your terminal if you already have a dashboard.helius.dev account.',
|
|
378
|
+
});
|
|
372
379
|
}
|
|
373
|
-
// ── Tier 3: full status via JWT ──
|
|
374
380
|
const projects = await listProjects(jwt, MCP_USER_AGENT);
|
|
375
381
|
if (projects.length === 0) {
|
|
376
|
-
return mcpError('No projects found. Call `
|
|
382
|
+
return mcpError('No projects found. Call `signup` first.', {
|
|
383
|
+
type: 'AUTH',
|
|
384
|
+
code: 'NO_PROJECT',
|
|
385
|
+
retryable: false,
|
|
386
|
+
recovery: 'Call `signup`.',
|
|
387
|
+
});
|
|
377
388
|
}
|
|
378
389
|
const projectId = projects[0].id;
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
390
|
+
// Pre-flight plan check. SDK will reject non-Agent plans, but its
|
|
391
|
+
// error path surfaces as a generic SDK_ERROR; intercepting here gives
|
|
392
|
+
// the agent a clean, structured error with the correct semantics.
|
|
393
|
+
const projectPlan = projects[0].subscription?.plan;
|
|
394
|
+
const normalizedPlan = projectPlan?.replace(/_v\d+$/, '');
|
|
395
|
+
if (normalizedPlan && normalizedPlan !== 'agent') {
|
|
396
|
+
return mcpError(`Prepaid credits via \`purchaseCredits\` are only available on the Agent plan. ` +
|
|
397
|
+
`Project ${projectId} is on the ${normalizedPlan} plan, where credit overage ` +
|
|
398
|
+
`is auto-billed at $5 per 1,000,000 credits on the next invoice — there's no ` +
|
|
399
|
+
`manual top-up flow. To preview your usage, call \`getAccountStatus\`.`, {
|
|
400
|
+
type: 'UNSUPPORTED',
|
|
401
|
+
code: 'UNSUPPORTED_PLAN',
|
|
402
|
+
retryable: false,
|
|
403
|
+
recovery: 'Use the dashboard to manage subscription overage, or call `getAccountStatus` to inspect usage.',
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
if (mode === 'autopay') {
|
|
407
|
+
let signerData;
|
|
408
|
+
try {
|
|
409
|
+
signerData = await loadSignerOrFail();
|
|
399
410
|
}
|
|
400
|
-
|
|
401
|
-
|
|
411
|
+
catch {
|
|
412
|
+
return mcpError('No keypair. Autopay needs one — call `generateKeypair` first.', {
|
|
413
|
+
type: 'AUTH',
|
|
414
|
+
code: 'NO_KEYPAIR',
|
|
415
|
+
retryable: false,
|
|
416
|
+
recovery: 'Call `generateKeypair`.',
|
|
417
|
+
});
|
|
402
418
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
let daysNote = '';
|
|
415
|
-
if (cycle) {
|
|
416
|
-
const end = new Date(cycle.end);
|
|
417
|
-
const now = new Date();
|
|
418
|
-
const daysLeft = Math.ceil((end.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
|
419
|
-
cycleStr = ` (${cycle.start} - ${cycle.end}, ${daysLeft} day${daysLeft !== 1 ? 's' : ''} remaining)`;
|
|
420
|
-
// Burn-rate warning: project usage over elapsed days to end of cycle
|
|
421
|
-
const start = new Date(cycle.start);
|
|
422
|
-
const totalDays = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
|
|
423
|
-
const elapsedDays = totalDays - daysLeft;
|
|
424
|
-
if (elapsedDays > 0 && daysLeft > 0) {
|
|
425
|
-
const projectedTotal = Math.round((usage.totalCreditsUsed / elapsedDays) * totalDays);
|
|
426
|
-
if (projectedTotal > total) {
|
|
427
|
-
const overageM = ((projectedTotal - total) / 1_000_000).toFixed(1);
|
|
428
|
-
daysNote = `\n> At current burn rate you're projected to use ~${(projectedTotal / 1_000_000).toFixed(1)}M credits this cycle — ${overageM}M over your ${(total / 1_000_000).toFixed(0)}M limit. Consider upgrading or reducing usage.`;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
lines.push('', `### Credits — Billing Cycle${cycleStr}`);
|
|
433
|
-
lines.push(`- **Remaining:** ${usage.remainingCredits.toLocaleString()} / ${total.toLocaleString()} (${pctRemaining}%)`);
|
|
434
|
-
lines.push(`- **Used:** ${usage.totalCreditsUsed.toLocaleString()} (${pctUsed}%)`);
|
|
435
|
-
lines.push(` - API: ${usage.apiUsage.toLocaleString()}`);
|
|
436
|
-
lines.push(` - RPC: ${usage.rpcUsage.toLocaleString()} | RPC GPA: ${usage.rpcGPAUsage.toLocaleString()} | Webhooks: ${usage.webhookUsage.toLocaleString()}`);
|
|
437
|
-
if (usage.overageCreditsUsed > 0) {
|
|
438
|
-
lines.push(`- **Overage:** ${usage.overageCreditsUsed.toLocaleString()} credits ($${usage.overageCost.toFixed(2)})`);
|
|
439
|
-
}
|
|
440
|
-
else {
|
|
441
|
-
lines.push(`- **Overage:** none`);
|
|
442
|
-
}
|
|
443
|
-
if (usage.remainingPrepaidCredits > 0 || usage.prepaidCreditsUsed > 0) {
|
|
444
|
-
lines.push(`- **Prepaid:** ${usage.remainingPrepaidCredits.toLocaleString()} remaining (${usage.prepaidCreditsUsed.toLocaleString()} used)`);
|
|
445
|
-
}
|
|
446
|
-
if (daysNote)
|
|
447
|
-
lines.push(daysNote);
|
|
448
|
-
// Low-credit warning
|
|
449
|
-
if (parseFloat(pctRemaining) < 20) {
|
|
450
|
-
lines.push(`\n> Less than 20% of credits remaining. Use \`previewUpgrade\` to see upgrade pricing, or \`getHeliusPlanInfo\` to compare plans.`);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
return mcpText(lines.join('\n'));
|
|
419
|
+
const result = await purchaseCreditsAndPay({
|
|
420
|
+
secretKey: signerData.secretKey,
|
|
421
|
+
jwt,
|
|
422
|
+
projectId,
|
|
423
|
+
qty,
|
|
424
|
+
couponCode,
|
|
425
|
+
});
|
|
426
|
+
return renderUpgradeOrCreditsAndPayResult(result, 'Credits top-up');
|
|
427
|
+
}
|
|
428
|
+
const result = await sdkPurchaseCredits({ jwt, projectId, qty, couponCode });
|
|
429
|
+
return mcpText(renderPaymentLink(result.paymentLink, 'Credits top-up', 'getAccountStatus'));
|
|
454
430
|
}
|
|
455
431
|
catch (err) {
|
|
456
|
-
return handleToolError(err, 'Error
|
|
432
|
+
return handleToolError(err, 'Error purchasing credits');
|
|
457
433
|
}
|
|
458
434
|
});
|
|
459
|
-
|
|
435
|
+
// ── Renewal Pay ──
|
|
436
|
+
server.tool('payRenewal', 'Pay an existing renewal payment intent. Default `mode: "link"` returns the hosted-checkout URL for the renewal. `mode: "autopay"` pays USDC from the local keypair.', {
|
|
437
|
+
mode: z.enum(['link', 'autopay']).default('link').describe('link (default) or autopay'),
|
|
460
438
|
paymentIntentId: z.string().describe('Payment intent ID from renewal notification'),
|
|
461
|
-
}, async ({ paymentIntentId }) => {
|
|
439
|
+
}, async ({ mode, paymentIntentId }) => {
|
|
462
440
|
try {
|
|
463
|
-
let signerData;
|
|
464
|
-
try {
|
|
465
|
-
signerData = await loadSignerOrFail();
|
|
466
|
-
}
|
|
467
|
-
catch {
|
|
468
|
-
return mcpError('No keypair found. Call `generateKeypair` first.');
|
|
469
|
-
}
|
|
470
441
|
const jwt = getJwt();
|
|
471
442
|
if (!jwt) {
|
|
472
|
-
return mcpError('Not authenticated. Call `
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
443
|
+
return mcpError('Not authenticated. Call `signup`, or run `helius login` in your terminal if you have a dashboard.helius.dev account.', {
|
|
444
|
+
type: 'AUTH',
|
|
445
|
+
code: 'NOT_AUTHENTICATED',
|
|
446
|
+
retryable: false,
|
|
447
|
+
recovery: 'Call `signup`, or run `helius login` in your terminal if you already have a dashboard.helius.dev account.',
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
if (mode === 'autopay') {
|
|
451
|
+
let signerData;
|
|
452
|
+
try {
|
|
453
|
+
signerData = await loadSignerOrFail();
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
return mcpError('No keypair. Autopay needs one — call `generateKeypair` first.', {
|
|
457
|
+
type: 'AUTH',
|
|
458
|
+
code: 'NO_KEYPAIR',
|
|
459
|
+
retryable: false,
|
|
460
|
+
recovery: 'Call `generateKeypair`.',
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
const result = await payRenewalAndPay(signerData.secretKey, jwt, paymentIntentId);
|
|
464
|
+
return renderUpgradeOrCreditsAndPayResult(result, 'Renewal');
|
|
480
465
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
(result.txSignature ? `- **TX:** \`${result.txSignature}\`\n` : '') +
|
|
484
|
-
`\nYour subscription has been renewed successfully.`);
|
|
466
|
+
const result = await payRenewal(jwt, paymentIntentId);
|
|
467
|
+
return mcpText(renderPaymentLink(result.paymentLink, 'Renewal', 'payRenewal again with mode: "autopay" or open the link'));
|
|
485
468
|
}
|
|
486
469
|
catch (err) {
|
|
487
470
|
return handleToolError(err, 'Error processing renewal payment');
|
|
488
471
|
}
|
|
489
472
|
});
|
|
490
473
|
}
|
|
474
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
475
|
+
// Resume / result rendering helpers
|
|
476
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
477
|
+
async function handleResumeSignup() {
|
|
478
|
+
const jwt = getJwt();
|
|
479
|
+
if (!jwt) {
|
|
480
|
+
return mcpError('No pending signup. Call `signup` (link mode) first.', {
|
|
481
|
+
type: 'AUTH',
|
|
482
|
+
code: 'NOT_AUTHENTICATED',
|
|
483
|
+
retryable: false,
|
|
484
|
+
recovery: 'Call `signup`.',
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
const projects = await listProjects(jwt, MCP_USER_AGENT);
|
|
488
|
+
if (projects.length === 0) {
|
|
489
|
+
return mcpText('Payment not yet provisioned on the backend. The webhook may still be processing — try again in a moment.');
|
|
490
|
+
}
|
|
491
|
+
const projectId = projects[0].id;
|
|
492
|
+
const details = await getProject(jwt, projectId, MCP_USER_AGENT);
|
|
493
|
+
const apiKey = details.apiKeys?.[0]?.keyId;
|
|
494
|
+
if (!apiKey) {
|
|
495
|
+
return mcpText('Project is provisioned but no API key has been generated yet. Try again in a moment.');
|
|
496
|
+
}
|
|
497
|
+
setApiKey(apiKey);
|
|
498
|
+
setSharedApiKey(apiKey);
|
|
499
|
+
return mcpText(`**Signup complete!**\n\n` +
|
|
500
|
+
`- **Project ID:** \`${projectId}\`\n` +
|
|
501
|
+
`- **API Key:** \`${apiKey}\`\n\n` +
|
|
502
|
+
`API key configured for this session and saved to \`${SHARED_CONFIG_PATH}\`. All Helius tools are ready to use.`);
|
|
503
|
+
}
|
|
504
|
+
function renderSignupAndPayResult(result) {
|
|
505
|
+
switch (result.kind) {
|
|
506
|
+
case 'completed':
|
|
507
|
+
setApiKey(result.apiKey);
|
|
508
|
+
setSharedApiKey(result.apiKey);
|
|
509
|
+
setJwt(result.jwt);
|
|
510
|
+
return mcpText(`**Helius Account Created**\n\n` +
|
|
511
|
+
`- **Project ID:** \`${result.projectId}\`\n` +
|
|
512
|
+
`- **API Key:** \`${result.apiKey}\`\n` +
|
|
513
|
+
`- **Mainnet RPC:** \`${result.endpoints.mainnet}\`\n` +
|
|
514
|
+
(result.txSignature ? `- **Payment TX:** \`${result.txSignature}\`\n` : '') +
|
|
515
|
+
`\nAPI key configured for this session and saved to \`${SHARED_CONFIG_PATH}\`.`);
|
|
516
|
+
case 'already_subscribed':
|
|
517
|
+
setApiKey(result.apiKey);
|
|
518
|
+
setSharedApiKey(result.apiKey);
|
|
519
|
+
setJwt(result.jwt);
|
|
520
|
+
return mcpText(`**Helius Account Found**\n\n` +
|
|
521
|
+
`You already have a Helius account on this plan. No payment was needed.\n\n` +
|
|
522
|
+
`- **Project ID:** \`${result.projectId}\`\n` +
|
|
523
|
+
`- **API Key:** \`${result.apiKey}\`\n` +
|
|
524
|
+
`\nAPI key configured for this session.`);
|
|
525
|
+
case 'upgrade_required':
|
|
526
|
+
return mcpText(`**Plan change required**\n\n` +
|
|
527
|
+
`Your wallet is already on plan \`${result.currentPlan}\`. ` +
|
|
528
|
+
`Use the \`upgradePlan\` tool to switch to \`${result.requestedPlan}\`.`);
|
|
529
|
+
case 'pending':
|
|
530
|
+
setJwt(result.jwt);
|
|
531
|
+
return mcpText(`**Payment sent — activation pending**\n\n` +
|
|
532
|
+
`USDC was sent (tx: \`${result.txSignature ?? 'unknown'}\`) but the backend hasn't finished provisioning yet.\n\n` +
|
|
533
|
+
`Call \`signup\` with \`mode: "resume"\` to try again.`);
|
|
534
|
+
case 'expired':
|
|
535
|
+
return mcpError(`Payment intent expired (\`${result.paymentIntentId}\`). Run \`signup\` again to start a fresh one.`, { type: 'API', code: 'EXPIRED', retryable: false, recovery: 'Run `signup` again.' });
|
|
536
|
+
case 'failed':
|
|
537
|
+
return mcpError(`Payment failed: ${result.reason ?? 'unknown reason'} (intent \`${result.paymentIntentId}\`).`, { type: 'API', code: 'OPERATION_FAILED', retryable: false, recovery: 'Contact support if this persists.' });
|
|
538
|
+
default: {
|
|
539
|
+
const _exhaustive = result;
|
|
540
|
+
throw new Error(`Unhandled result kind: ${JSON.stringify(_exhaustive)}`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function renderUpgradeOrCreditsAndPayResult(result, flowName) {
|
|
545
|
+
switch (result.kind) {
|
|
546
|
+
case 'completed':
|
|
547
|
+
return mcpText(`**${flowName} complete!**\n\n` +
|
|
548
|
+
(result.txSignature ? `- **TX:** \`${result.txSignature}\`\n` : '') +
|
|
549
|
+
`- **Payment Intent:** \`${result.paymentIntentId}\``);
|
|
550
|
+
case 'pending':
|
|
551
|
+
return mcpText(`**Payment sent — activation pending**\n\n` +
|
|
552
|
+
`USDC was sent (tx: \`${result.txSignature ?? 'unknown'}\`). The backend is still processing.\n\n` +
|
|
553
|
+
`Call this tool again or check \`getAccountStatus\` shortly.`);
|
|
554
|
+
case 'expired':
|
|
555
|
+
return mcpError(`Payment intent expired (\`${result.paymentIntentId}\`). Run the tool again to start fresh.`, { type: 'API', code: 'EXPIRED', retryable: false, recovery: 'Retry from scratch.' });
|
|
556
|
+
case 'failed':
|
|
557
|
+
return mcpError(`Payment failed: ${result.reason ?? 'unknown reason'} (intent \`${result.paymentIntentId}\`).`, { type: 'API', code: 'OPERATION_FAILED', retryable: false, recovery: 'Contact support if this persists.' });
|
|
558
|
+
default: {
|
|
559
|
+
const _exhaustive = result;
|
|
560
|
+
throw new Error(`Unhandled result kind: ${JSON.stringify(_exhaustive)}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|