agentwallet-mcp 1.9.0 → 1.10.1
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/README.md +326 -294
- package/build/index.js +194 -130
- package/build/local-solana.d.ts +58 -0
- package/build/local-solana.js +252 -0
- package/build/ssrf-guard.d.ts +38 -0
- package/build/ssrf-guard.js +178 -0
- package/package.json +68 -66
package/build/index.js
CHANGED
|
@@ -12,8 +12,14 @@
|
|
|
12
12
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
13
13
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
14
14
|
import { z } from 'zod';
|
|
15
|
+
import { assertPublicUrl, safeFetch } from './ssrf-guard.js';
|
|
15
16
|
import { deriveX402Payment, isWithinCap } from './x402-payment.js';
|
|
16
17
|
import { isLocalMode, getLocalAddress, localWalletRecord, localSend, localNativeBalance, localTokenBalance, localSignMessage, resolveRpcUrl, } from './local-wallet.js';
|
|
18
|
+
import { isSolanaLocalMode, getSolanaAddress, solanaWalletRecord, localSolBalance, localSplBalance, localSolTransfer, localSplTransfer, resolveSolanaRpc, } from './local-solana.js';
|
|
19
|
+
/** True when either chain family is running non-custodially. */
|
|
20
|
+
function anyLocalMode() {
|
|
21
|
+
return isLocalMode() || isSolanaLocalMode();
|
|
22
|
+
}
|
|
17
23
|
// ─── Configuration ──────────────────────────────────────────────
|
|
18
24
|
const API_BASE = process.env.AGENTWALLET_API_URL || 'https://hifriendbot.com/wp-json/agentwallet/v1';
|
|
19
25
|
const API_USER = process.env.AGENTWALLET_USER || '';
|
|
@@ -38,21 +44,43 @@ function localChainId(body, qs) {
|
|
|
38
44
|
return parseInt(fromEnv, 10);
|
|
39
45
|
return 8453; // Base, matching the custodial default
|
|
40
46
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Refuse rather than silently hand the operation to someone else's signer.
|
|
49
|
+
*
|
|
50
|
+
* Reached when the operator is running non-custodially for one chain family but
|
|
51
|
+
* has supplied no key for the other. Falling through to the hosted API here
|
|
52
|
+
* would move funds onto a key they do not hold, while they believe they are in
|
|
53
|
+
* self-custody. That is the single worst thing this server could do, so it
|
|
54
|
+
* stops and says exactly which variable is missing.
|
|
55
|
+
*/
|
|
56
|
+
function refuseMissingLocalKey(family) {
|
|
57
|
+
const wanted = family === 'solana'
|
|
58
|
+
? 'AGENTWALLET_SOLANA_KEY (or AGENTWALLET_SOLANA_KEYFILE)'
|
|
59
|
+
: 'AGENTWALLET_PRIVATE_KEY (or AGENTWALLET_KEYFILE)';
|
|
60
|
+
throw new Error(`This is a ${family === 'solana' ? 'Solana' : 'EVM'} operation and no local ${family === 'solana' ? 'Solana' : 'EVM'} key is configured, ` +
|
|
61
|
+
`but this server is running in local signing mode. Refusing rather than routing it through the hosted ` +
|
|
62
|
+
`custodial signer while you believe you hold the keys. Set ${wanted} to sign it here, or unset the other ` +
|
|
63
|
+
`local key variables to use the hosted path for everything.`);
|
|
45
64
|
}
|
|
46
65
|
async function routeLocally(path, method, body) {
|
|
47
66
|
const [rawPath, rawQs = ''] = path.split('?');
|
|
48
67
|
const qs = new URLSearchParams(rawQs);
|
|
49
68
|
// POST /wallets/{id}/send
|
|
50
69
|
if (method === 'POST' && /^\/wallets\/[^/]*\/send$/.test(rawPath)) {
|
|
51
|
-
if (body?.token_mint)
|
|
52
|
-
refuseSolanaLocally();
|
|
53
70
|
const chainId = localChainId(body, qs);
|
|
54
|
-
|
|
55
|
-
|
|
71
|
+
const solanaOp = SOLANA_CHAIN_IDS.has(chainId) || Boolean(body?.token_mint);
|
|
72
|
+
if (solanaOp) {
|
|
73
|
+
if (!isSolanaLocalMode())
|
|
74
|
+
refuseMissingLocalKey('solana');
|
|
75
|
+
const to = String(body?.to || '');
|
|
76
|
+
const value = String(body?.value ?? '0');
|
|
77
|
+
if (body?.token_mint) {
|
|
78
|
+
return localSplTransfer(String(body.token_mint), to, value, Number(body.token_decimals ?? 6), chainId);
|
|
79
|
+
}
|
|
80
|
+
return localSolTransfer(to, value, chainId);
|
|
81
|
+
}
|
|
82
|
+
if (!isLocalMode())
|
|
83
|
+
refuseMissingLocalKey('evm');
|
|
56
84
|
const to = String(body?.to || '');
|
|
57
85
|
const value = BigInt(String(body?.value ?? '0'));
|
|
58
86
|
const data = String(body?.data || '');
|
|
@@ -65,16 +93,27 @@ async function routeLocally(path, method, body) {
|
|
|
65
93
|
// GET /wallets/{id}/balance
|
|
66
94
|
if (method === 'GET' && /^\/wallets\/[^/]*\/balance$/.test(rawPath)) {
|
|
67
95
|
const chainId = localChainId(body, qs);
|
|
68
|
-
if (SOLANA_CHAIN_IDS.has(chainId))
|
|
69
|
-
|
|
96
|
+
if (SOLANA_CHAIN_IDS.has(chainId)) {
|
|
97
|
+
if (!isSolanaLocalMode())
|
|
98
|
+
refuseMissingLocalKey('solana');
|
|
99
|
+
return localSolBalance(chainId);
|
|
100
|
+
}
|
|
101
|
+
if (!isLocalMode())
|
|
102
|
+
refuseMissingLocalKey('evm');
|
|
70
103
|
return localNativeBalance(chainId);
|
|
71
104
|
}
|
|
72
105
|
// GET /wallets/{id}/token-balance
|
|
73
106
|
if (method === 'GET' && /^\/wallets\/[^/]*\/token-balance$/.test(rawPath)) {
|
|
74
107
|
const chainId = localChainId(body, qs);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
108
|
+
const token0 = qs.get('token') || qs.get('token_address') || '';
|
|
109
|
+
if (SOLANA_CHAIN_IDS.has(chainId)) {
|
|
110
|
+
if (!isSolanaLocalMode())
|
|
111
|
+
refuseMissingLocalKey('solana');
|
|
112
|
+
return localSplBalance(token0, chainId);
|
|
113
|
+
}
|
|
114
|
+
if (!isLocalMode())
|
|
115
|
+
refuseMissingLocalKey('evm');
|
|
116
|
+
const token = token0;
|
|
78
117
|
if (!/^0x[a-fA-F0-9]{40}$/.test(token)) {
|
|
79
118
|
throw new Error(`Local mode needs an EVM token address, got "${token}".`);
|
|
80
119
|
}
|
|
@@ -88,11 +127,18 @@ async function routeLocally(path, method, body) {
|
|
|
88
127
|
throw new Error('Local mode signs messages in-process. For transactions use send_transaction, ' +
|
|
89
128
|
'which signs and broadcasts locally in one step.');
|
|
90
129
|
}
|
|
91
|
-
// Wallet listing and lookup describe
|
|
92
|
-
if (method === 'GET' && rawPath === '/wallets')
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
130
|
+
// Wallet listing and lookup describe whichever local keys are configured.
|
|
131
|
+
if (method === 'GET' && rawPath === '/wallets') {
|
|
132
|
+
const wallets = [];
|
|
133
|
+
if (isLocalMode())
|
|
134
|
+
wallets.push(localWalletRecord());
|
|
135
|
+
if (isSolanaLocalMode())
|
|
136
|
+
wallets.push(solanaWalletRecord());
|
|
137
|
+
return { wallets };
|
|
138
|
+
}
|
|
139
|
+
if (method === 'GET' && /^\/wallets\/[^/]+$/.test(rawPath)) {
|
|
140
|
+
return isLocalMode() ? localWalletRecord() : solanaWalletRecord();
|
|
141
|
+
}
|
|
96
142
|
if (method === 'POST' && rawPath === '/wallets') {
|
|
97
143
|
throw new Error('Local signing mode uses the single key you supplied, so there is nothing to create. ' +
|
|
98
144
|
'Generate another key yourself and run a second instance with a different ' +
|
|
@@ -112,7 +158,7 @@ async function api(path, method = 'GET', body, extraHeaders) {
|
|
|
112
158
|
/* Local mode intercepts here rather than inside each tool. Routing at the
|
|
113
159
|
single choke point every funds operation already passes through means no
|
|
114
160
|
tool can quietly stay custodial because someone forgot to update it. */
|
|
115
|
-
if (
|
|
161
|
+
if (anyLocalMode()) {
|
|
116
162
|
const handled = await routeLocally(path, method, body);
|
|
117
163
|
if (handled !== NOT_HANDLED)
|
|
118
164
|
return handled;
|
|
@@ -132,7 +178,7 @@ async function api(path, method = 'GET', body, extraHeaders) {
|
|
|
132
178
|
const res = await fetch(url, options);
|
|
133
179
|
const data = await res.json();
|
|
134
180
|
// Handle 402 Payment Required — auto-pay if a wallet or a local key is configured
|
|
135
|
-
if (res.status === 402 && (X402_WALLET_ID ||
|
|
181
|
+
if (res.status === 402 && (X402_WALLET_ID || anyLocalMode()) && !extraHeaders?.['X-PAYMENT']) {
|
|
136
182
|
return handleX402Payment(data, path, method, body);
|
|
137
183
|
}
|
|
138
184
|
if (!res.ok) {
|
|
@@ -315,82 +361,82 @@ function resolveChainId(network) {
|
|
|
315
361
|
// ─── Server ──────────────────────────────────────────────────────
|
|
316
362
|
const server = new McpServer({
|
|
317
363
|
name: 'agentwallet',
|
|
318
|
-
version: '1.
|
|
364
|
+
version: '1.10.1',
|
|
319
365
|
}, {
|
|
320
|
-
instructions: `AgentWallet gives AI agents their own blockchain wallets. Private keys are encrypted server-side and never exposed — agents sign and broadcast transactions without ever touching raw keys.
|
|
321
|
-
|
|
322
|
-
## Getting Started
|
|
323
|
-
1. Call list_wallets to see existing wallets, or create_wallet to make a new one.
|
|
324
|
-
2. Fund the wallet by sending tokens to its address from an external source.
|
|
325
|
-
3. Use transfer (native tokens) or transfer_token (ERC-20/SPL tokens) to send funds.
|
|
326
|
-
|
|
327
|
-
## Supported Chains
|
|
328
|
-
- **EVM chains:** Ethereum (1), Base (8453), Arbitrum (42161), Optimism (10), Polygon (137), Avalanche (43114), BSC (56), Zora (7777777), PulseChain (369)
|
|
329
|
-
- **Solana:** Mainnet (900), Devnet (901)
|
|
330
|
-
- Call get_chains for the full list with native tokens and stablecoin contract addresses.
|
|
331
|
-
|
|
332
|
-
## EVM vs Solana
|
|
333
|
-
- **Addresses:** EVM uses 0x-prefixed hex (42 chars). Solana uses Base58 (32-44 chars).
|
|
334
|
-
- **Native tokens:** EVM = ETH/POL/BNB/etc (18 decimals, wei). Solana = SOL (9 decimals, lamports).
|
|
335
|
-
- **Token standards:** EVM uses ERC-20. Solana uses SPL tokens.
|
|
336
|
-
- **EVM-only tools:** approve_token, get_allowance, wrap_eth, unwrap_eth, call_contract, get_token_info. These do not work on Solana.
|
|
337
|
-
- **Works on both:** create_wallet, transfer, transfer_token, send_transaction, sign_transaction, get_balance, get_token_balance.
|
|
338
|
-
|
|
339
|
-
## Common Workflows
|
|
340
|
-
|
|
341
|
-
### Send native tokens (ETH, SOL, etc.)
|
|
342
|
-
Use the transfer tool with a human-readable amount (e.g. "0.1" ETH). It auto-converts to wei/lamports.
|
|
343
|
-
|
|
344
|
-
### Send ERC-20 or SPL tokens
|
|
345
|
-
Use transfer_token. You need: wallet_id, token contract address, recipient, amount, chain_id, and decimals (6 for USDC, 18 for most ERC-20s). Call get_chains to find stablecoin addresses.
|
|
346
|
-
|
|
347
|
-
### Interact with DeFi (EVM only)
|
|
348
|
-
1. Check allowance with get_allowance.
|
|
349
|
-
2. If needed, call approve_token to let the DeFi contract spend your tokens.
|
|
350
|
-
3. Use send_transaction with the contract's calldata to execute the interaction.
|
|
351
|
-
|
|
352
|
-
### Wrap/unwrap native tokens (EVM only)
|
|
353
|
-
Use wrap_eth to convert ETH → WETH (required by many DeFi protocols). Use unwrap_eth to convert back.
|
|
354
|
-
|
|
355
|
-
### Read on-chain data (EVM only)
|
|
356
|
-
Use call_contract for gas-free read-only calls (eth_call). Provide ABI-encoded calldata.
|
|
357
|
-
|
|
358
|
-
## x402 Payments — Paying for Resources
|
|
359
|
-
The pay_x402 tool handles the full x402 payment flow automatically:
|
|
360
|
-
1. Fetches the URL.
|
|
361
|
-
2. If the server returns HTTP 402, parses the payment requirements.
|
|
362
|
-
3. Pays on-chain using your wallet.
|
|
363
|
-
4. Retries the request with proof of payment.
|
|
364
|
-
Always set max_payment to prevent overspending (e.g. "1.00" for max 1 USDC).
|
|
365
|
-
|
|
366
|
-
## x402 Paywalls — Accepting Payments
|
|
367
|
-
Create paywalls to charge other agents for accessing your resources:
|
|
368
|
-
1. create_paywall — set price, token, chain, and the resource URL to protect.
|
|
369
|
-
2. Share the access URL. Clients that pay on-chain get the resource; others get HTTP 402.
|
|
370
|
-
3. list_paywalls and get_paywall_payments to track revenue.
|
|
371
|
-
4. get_x402_revenue for aggregate stats across all paywalls.
|
|
372
|
-
|
|
373
|
-
## Wallet Security
|
|
374
|
-
- Use pause_wallet to immediately freeze a wallet if compromised. No transactions can be signed while paused.
|
|
375
|
-
- Use unpause_wallet to resume operations.
|
|
376
|
-
- Use delete_wallet to permanently disable a wallet.
|
|
377
|
-
- Wallets have server-enforced spending limits.
|
|
378
|
-
|
|
379
|
-
## Tool Selection Guide
|
|
380
|
-
| Goal | Tool |
|
|
381
|
-
|------|------|
|
|
382
|
-
| Send ETH/SOL/native tokens | transfer |
|
|
383
|
-
| Send USDC/ERC-20/SPL tokens | transfer_token |
|
|
384
|
-
| Check native balance | get_balance |
|
|
385
|
-
| Check token balance | get_token_balance |
|
|
386
|
-
| Look up token details | get_token_info (EVM only) |
|
|
387
|
-
| Approve DeFi spending | approve_token (EVM only) |
|
|
388
|
-
| Check approval amount | get_allowance (EVM only) |
|
|
389
|
-
| Read smart contract | call_contract (EVM only) |
|
|
390
|
-
| Custom transaction | send_transaction |
|
|
391
|
-
| Sign without broadcasting | sign_transaction |
|
|
392
|
-
| Pay for x402 resource | pay_x402 |
|
|
393
|
-
| Charge for your resource | create_paywall |
|
|
366
|
+
instructions: `AgentWallet gives AI agents their own blockchain wallets. Private keys are encrypted server-side and never exposed — agents sign and broadcast transactions without ever touching raw keys.
|
|
367
|
+
|
|
368
|
+
## Getting Started
|
|
369
|
+
1. Call list_wallets to see existing wallets, or create_wallet to make a new one.
|
|
370
|
+
2. Fund the wallet by sending tokens to its address from an external source.
|
|
371
|
+
3. Use transfer (native tokens) or transfer_token (ERC-20/SPL tokens) to send funds.
|
|
372
|
+
|
|
373
|
+
## Supported Chains
|
|
374
|
+
- **EVM chains:** Ethereum (1), Base (8453), Arbitrum (42161), Optimism (10), Polygon (137), Avalanche (43114), BSC (56), Zora (7777777), PulseChain (369)
|
|
375
|
+
- **Solana:** Mainnet (900), Devnet (901)
|
|
376
|
+
- Call get_chains for the full list with native tokens and stablecoin contract addresses.
|
|
377
|
+
|
|
378
|
+
## EVM vs Solana
|
|
379
|
+
- **Addresses:** EVM uses 0x-prefixed hex (42 chars). Solana uses Base58 (32-44 chars).
|
|
380
|
+
- **Native tokens:** EVM = ETH/POL/BNB/etc (18 decimals, wei). Solana = SOL (9 decimals, lamports).
|
|
381
|
+
- **Token standards:** EVM uses ERC-20. Solana uses SPL tokens.
|
|
382
|
+
- **EVM-only tools:** approve_token, get_allowance, wrap_eth, unwrap_eth, call_contract, get_token_info. These do not work on Solana.
|
|
383
|
+
- **Works on both:** create_wallet, transfer, transfer_token, send_transaction, sign_transaction, get_balance, get_token_balance.
|
|
384
|
+
|
|
385
|
+
## Common Workflows
|
|
386
|
+
|
|
387
|
+
### Send native tokens (ETH, SOL, etc.)
|
|
388
|
+
Use the transfer tool with a human-readable amount (e.g. "0.1" ETH). It auto-converts to wei/lamports.
|
|
389
|
+
|
|
390
|
+
### Send ERC-20 or SPL tokens
|
|
391
|
+
Use transfer_token. You need: wallet_id, token contract address, recipient, amount, chain_id, and decimals (6 for USDC, 18 for most ERC-20s). Call get_chains to find stablecoin addresses.
|
|
392
|
+
|
|
393
|
+
### Interact with DeFi (EVM only)
|
|
394
|
+
1. Check allowance with get_allowance.
|
|
395
|
+
2. If needed, call approve_token to let the DeFi contract spend your tokens.
|
|
396
|
+
3. Use send_transaction with the contract's calldata to execute the interaction.
|
|
397
|
+
|
|
398
|
+
### Wrap/unwrap native tokens (EVM only)
|
|
399
|
+
Use wrap_eth to convert ETH → WETH (required by many DeFi protocols). Use unwrap_eth to convert back.
|
|
400
|
+
|
|
401
|
+
### Read on-chain data (EVM only)
|
|
402
|
+
Use call_contract for gas-free read-only calls (eth_call). Provide ABI-encoded calldata.
|
|
403
|
+
|
|
404
|
+
## x402 Payments — Paying for Resources
|
|
405
|
+
The pay_x402 tool handles the full x402 payment flow automatically:
|
|
406
|
+
1. Fetches the URL.
|
|
407
|
+
2. If the server returns HTTP 402, parses the payment requirements.
|
|
408
|
+
3. Pays on-chain using your wallet.
|
|
409
|
+
4. Retries the request with proof of payment.
|
|
410
|
+
Always set max_payment to prevent overspending (e.g. "1.00" for max 1 USDC).
|
|
411
|
+
|
|
412
|
+
## x402 Paywalls — Accepting Payments
|
|
413
|
+
Create paywalls to charge other agents for accessing your resources:
|
|
414
|
+
1. create_paywall — set price, token, chain, and the resource URL to protect.
|
|
415
|
+
2. Share the access URL. Clients that pay on-chain get the resource; others get HTTP 402.
|
|
416
|
+
3. list_paywalls and get_paywall_payments to track revenue.
|
|
417
|
+
4. get_x402_revenue for aggregate stats across all paywalls.
|
|
418
|
+
|
|
419
|
+
## Wallet Security
|
|
420
|
+
- Use pause_wallet to immediately freeze a wallet if compromised. No transactions can be signed while paused.
|
|
421
|
+
- Use unpause_wallet to resume operations.
|
|
422
|
+
- Use delete_wallet to permanently disable a wallet.
|
|
423
|
+
- Wallets have server-enforced spending limits.
|
|
424
|
+
|
|
425
|
+
## Tool Selection Guide
|
|
426
|
+
| Goal | Tool |
|
|
427
|
+
|------|------|
|
|
428
|
+
| Send ETH/SOL/native tokens | transfer |
|
|
429
|
+
| Send USDC/ERC-20/SPL tokens | transfer_token |
|
|
430
|
+
| Check native balance | get_balance |
|
|
431
|
+
| Check token balance | get_token_balance |
|
|
432
|
+
| Look up token details | get_token_info (EVM only) |
|
|
433
|
+
| Approve DeFi spending | approve_token (EVM only) |
|
|
434
|
+
| Check approval amount | get_allowance (EVM only) |
|
|
435
|
+
| Read smart contract | call_contract (EVM only) |
|
|
436
|
+
| Custom transaction | send_transaction |
|
|
437
|
+
| Sign without broadcasting | sign_transaction |
|
|
438
|
+
| Pay for x402 resource | pay_x402 |
|
|
439
|
+
| Charge for your resource | create_paywall |
|
|
394
440
|
| Check usage/billing | get_usage |`,
|
|
395
441
|
});
|
|
396
442
|
// ─── Tool: create_wallet ─────────────────────────────────────────
|
|
@@ -855,19 +901,10 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
|
|
|
855
901
|
throw new Error('Invalid headers JSON. Must be a JSON object (e.g. {"Authorization": "Bearer ..."}).');
|
|
856
902
|
}
|
|
857
903
|
}
|
|
858
|
-
// Validate URL
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
const blockedPrefixes = ['10.', '172.16.', '172.17.', '172.18.', '172.19.', '172.20.', '172.21.',
|
|
863
|
-
'172.22.', '172.23.', '172.24.', '172.25.', '172.26.', '172.27.', '172.28.', '172.29.',
|
|
864
|
-
'172.30.', '172.31.', '192.168.'];
|
|
865
|
-
if (blockedHosts.includes(hostname) || blockedPrefixes.some(p => hostname.startsWith(p))) {
|
|
866
|
-
throw new Error('URL points to a private/internal address. Only public URLs are allowed.');
|
|
867
|
-
}
|
|
868
|
-
if (urlObj.protocol !== 'https:') {
|
|
869
|
-
throw new Error('Only HTTPS URLs are supported for x402 payments.');
|
|
870
|
-
}
|
|
904
|
+
// Validate URL: canonicalizes IP literals (including IPv4-mapped IPv6) and
|
|
905
|
+
// resolves DNS names, rejecting any private/internal destination. Redirect
|
|
906
|
+
// hops are re-validated by safeFetch below.
|
|
907
|
+
await assertPublicUrl(url);
|
|
871
908
|
// Step 1: Make initial request
|
|
872
909
|
const reqOptions = {
|
|
873
910
|
method,
|
|
@@ -879,7 +916,7 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
|
|
|
879
916
|
if (!reqHeaders['Content-Type'])
|
|
880
917
|
reqHeaders['Content-Type'] = 'application/json';
|
|
881
918
|
}
|
|
882
|
-
const initialRes = await
|
|
919
|
+
const initialRes = await safeFetch(url, reqOptions);
|
|
883
920
|
// If not 402, return the response as-is (no payment needed)
|
|
884
921
|
if (initialRes.status !== 402) {
|
|
885
922
|
const text = await initialRes.text();
|
|
@@ -1013,7 +1050,7 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
|
|
|
1013
1050
|
if (reqBody && method !== 'GET') {
|
|
1014
1051
|
retryOptions.body = reqBody;
|
|
1015
1052
|
}
|
|
1016
|
-
const retryRes = await
|
|
1053
|
+
const retryRes = await safeFetch(url, retryOptions);
|
|
1017
1054
|
const retryText = await retryRes.text();
|
|
1018
1055
|
let retryParsed;
|
|
1019
1056
|
try {
|
|
@@ -1193,31 +1230,53 @@ server.tool('get_x402_revenue', 'Get aggregate x402 revenue statistics across al
|
|
|
1193
1230
|
// ─── Tool: wallet_mode ──────────────────────────────────────────
|
|
1194
1231
|
server.tool('wallet_mode', 'Report whether this server is signing locally (self-custody, the private key never leaves this machine) ' +
|
|
1195
1232
|
'or through the hosted AgentWallet API (custodial). Use this to verify custody before moving funds.', {}, async () => {
|
|
1196
|
-
if (!
|
|
1233
|
+
if (!anyLocalMode()) {
|
|
1197
1234
|
return jsonResponse({
|
|
1198
1235
|
mode: 'custodial',
|
|
1199
1236
|
custody: 'agentwallet',
|
|
1200
1237
|
signing: 'AgentWallet servers hold an encrypted key and sign on your behalf.',
|
|
1201
1238
|
api_base: API_BASE,
|
|
1202
|
-
to_self_custody: 'Set AGENTWALLET_PRIVATE_KEY
|
|
1239
|
+
to_self_custody: 'Set AGENTWALLET_PRIVATE_KEY for EVM and/or AGENTWALLET_SOLANA_KEY for Solana, then restart. ' +
|
|
1203
1240
|
'Use export_wallet_key first if you want to carry an existing hosted wallet across.',
|
|
1204
1241
|
});
|
|
1205
1242
|
}
|
|
1206
|
-
|
|
1207
|
-
try {
|
|
1208
|
-
rpc = resolveRpcUrl(parseInt(process.env.AGENTWALLET_CHAIN_ID || '8453', 10));
|
|
1209
|
-
}
|
|
1210
|
-
catch { /* chain has no default; not worth failing the report over */ }
|
|
1211
|
-
return jsonResponse({
|
|
1243
|
+
const report = {
|
|
1212
1244
|
mode: 'local',
|
|
1213
1245
|
custody: 'self',
|
|
1214
|
-
|
|
1215
|
-
signing: 'Signed in this process. The private key is never sent to AgentWallet or anyone else.',
|
|
1216
|
-
rpc_endpoint: rpc,
|
|
1217
|
-
chains: 'EVM only in local mode. Solana calls are refused rather than routed to the custodial API.',
|
|
1218
|
-
per_tx_cap_native: process.env.AGENTWALLET_MAX_TX_NATIVE || 'not set',
|
|
1246
|
+
signing: 'Signed in this process. Keys are never sent to AgentWallet or anyone else.',
|
|
1219
1247
|
max_autopay: process.env.AGENTWALLET_MAX_AUTOPAY || '1',
|
|
1220
|
-
}
|
|
1248
|
+
};
|
|
1249
|
+
if (isLocalMode()) {
|
|
1250
|
+
let rpc = 'default public endpoint';
|
|
1251
|
+
try {
|
|
1252
|
+
rpc = resolveRpcUrl(parseInt(process.env.AGENTWALLET_CHAIN_ID || '8453', 10));
|
|
1253
|
+
}
|
|
1254
|
+
catch { /* chain has no default; not worth failing the report over */ }
|
|
1255
|
+
report.evm = {
|
|
1256
|
+
address: getLocalAddress(),
|
|
1257
|
+
rpc_endpoint: rpc,
|
|
1258
|
+
per_tx_cap_native: process.env.AGENTWALLET_MAX_TX_NATIVE || 'not set',
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
else {
|
|
1262
|
+
report.evm = 'no local EVM key. EVM operations are refused, not sent to the hosted signer.';
|
|
1263
|
+
}
|
|
1264
|
+
if (isSolanaLocalMode()) {
|
|
1265
|
+
let rpc = 'default public endpoint';
|
|
1266
|
+
try {
|
|
1267
|
+
rpc = resolveSolanaRpc(900);
|
|
1268
|
+
}
|
|
1269
|
+
catch { /* fall through to the default label */ }
|
|
1270
|
+
report.solana = {
|
|
1271
|
+
address: getSolanaAddress(),
|
|
1272
|
+
rpc_endpoint: rpc,
|
|
1273
|
+
per_tx_cap_sol: process.env.AGENTWALLET_MAX_TX_SOL || 'not set',
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
else {
|
|
1277
|
+
report.solana = 'no local Solana key. Solana operations are refused, not sent to the hosted signer.';
|
|
1278
|
+
}
|
|
1279
|
+
return jsonResponse(report);
|
|
1221
1280
|
});
|
|
1222
1281
|
// ─── Tool: export_wallet_key ────────────────────────────────────
|
|
1223
1282
|
server.tool('export_wallet_key', 'Explain how to export the private key of a hosted (custodial) AgentWallet so it can be moved ' +
|
|
@@ -1225,12 +1284,12 @@ server.tool('export_wallet_key', 'Explain how to export the private key of a hos
|
|
|
1225
1284
|
'performed by this tool.', {
|
|
1226
1285
|
wallet_id: z.number().int().optional().describe('Hosted wallet ID to export'),
|
|
1227
1286
|
}, async ({ wallet_id }) => {
|
|
1228
|
-
if (
|
|
1287
|
+
if (anyLocalMode()) {
|
|
1229
1288
|
return jsonResponse({
|
|
1230
1289
|
exportable: true,
|
|
1231
1290
|
mode: 'local',
|
|
1232
|
-
note: 'You are already in self-custody. The
|
|
1233
|
-
'or
|
|
1291
|
+
note: 'You are already in self-custody. The keys are the ones you supplied via AGENTWALLET_PRIVATE_KEY, ' +
|
|
1292
|
+
'AGENTWALLET_SOLANA_KEY or their KEYFILE variants, and this server keeps no copy beyond this process.',
|
|
1234
1293
|
});
|
|
1235
1294
|
}
|
|
1236
1295
|
return jsonResponse({
|
|
@@ -1252,12 +1311,17 @@ async function main() {
|
|
|
1252
1311
|
await server.connect(transport);
|
|
1253
1312
|
/* stderr, so it never corrupts the stdio JSON-RPC stream. Announcing custody
|
|
1254
1313
|
at startup means an operator sees which mode they are in without asking. */
|
|
1255
|
-
if (
|
|
1314
|
+
if (anyLocalMode()) {
|
|
1256
1315
|
try {
|
|
1257
|
-
|
|
1316
|
+
const parts = [];
|
|
1317
|
+
if (isLocalMode())
|
|
1318
|
+
parts.push(`EVM ${getLocalAddress()}`);
|
|
1319
|
+
if (isSolanaLocalMode())
|
|
1320
|
+
parts.push(`Solana ${getSolanaAddress()}`);
|
|
1321
|
+
console.error(`AgentWallet MCP: LOCAL signing mode. ${parts.join(', ')}. Keys never leave this machine.`);
|
|
1258
1322
|
}
|
|
1259
1323
|
catch (e) {
|
|
1260
|
-
console.error(`AgentWallet MCP: local signing configured but
|
|
1324
|
+
console.error(`AgentWallet MCP: local signing configured but a key could not be loaded: ${e.message}`);
|
|
1261
1325
|
process.exit(1);
|
|
1262
1326
|
}
|
|
1263
1327
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local (non-custodial) Solana signing.
|
|
3
|
+
*
|
|
4
|
+
* Same contract as local-wallet.ts on the EVM side: the secret key is loaded
|
|
5
|
+
* once into this process, never written anywhere, never logged, and never put
|
|
6
|
+
* in an error message. Transactions are signed here and submitted straight to
|
|
7
|
+
* an RPC endpoint. The AgentWallet API is not involved.
|
|
8
|
+
*
|
|
9
|
+
* SPL transfers are built from raw instructions rather than @solana/spl-token
|
|
10
|
+
* on purpose. That package pulls in bigint-buffer, which carries a high
|
|
11
|
+
* severity buffer overflow advisory, and a wallet has no business shipping
|
|
12
|
+
* that when the two instructions it needs are a dozen lines each.
|
|
13
|
+
*/
|
|
14
|
+
import { Keypair } from '@solana/web3.js';
|
|
15
|
+
export declare function isSolanaLocalMode(): boolean;
|
|
16
|
+
export declare function getSolanaKeypair(): Keypair;
|
|
17
|
+
export declare function getSolanaAddress(): string;
|
|
18
|
+
export declare function resolveSolanaRpc(chainId?: number): string;
|
|
19
|
+
export declare function localSolBalance(chainId?: number): Promise<{
|
|
20
|
+
address: string;
|
|
21
|
+
chain_id: number;
|
|
22
|
+
balance_lamports: string;
|
|
23
|
+
balance: string;
|
|
24
|
+
mode: string;
|
|
25
|
+
}>;
|
|
26
|
+
export declare function localSplBalance(mintStr: string, chainId?: number): Promise<{
|
|
27
|
+
address: string;
|
|
28
|
+
chain_id: number;
|
|
29
|
+
mint: string;
|
|
30
|
+
token_account: string;
|
|
31
|
+
balance_raw: string;
|
|
32
|
+
balance: string;
|
|
33
|
+
decimals: number;
|
|
34
|
+
mode: string;
|
|
35
|
+
}>;
|
|
36
|
+
export interface LocalSolResult {
|
|
37
|
+
signature: string;
|
|
38
|
+
from: string;
|
|
39
|
+
to: string;
|
|
40
|
+
value: string;
|
|
41
|
+
chain_id: number;
|
|
42
|
+
mode: 'local';
|
|
43
|
+
signed_locally: true;
|
|
44
|
+
}
|
|
45
|
+
export declare function localSolTransfer(to: string, lamports: string, chainId?: number): Promise<LocalSolResult>;
|
|
46
|
+
/**
|
|
47
|
+
* Transfer SPL tokens. The recipient's associated account is created if it does
|
|
48
|
+
* not exist, using the idempotent instruction so a race cannot fail the send.
|
|
49
|
+
*/
|
|
50
|
+
export declare function localSplTransfer(mintStr: string, to: string, rawAmount: string, decimals: number, chainId?: number): Promise<LocalSolResult>;
|
|
51
|
+
export declare function solanaWalletRecord(): {
|
|
52
|
+
id: string;
|
|
53
|
+
address: string;
|
|
54
|
+
wallet_type: string;
|
|
55
|
+
mode: string;
|
|
56
|
+
custody: string;
|
|
57
|
+
note: string;
|
|
58
|
+
};
|