agentwallet-mcp 1.13.14 → 1.13.16

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/build/index.js CHANGED
@@ -588,6 +588,13 @@ const FORBIDDEN_REQUEST_HEADERS = new Set(['host', 'content-length', 'transfer-e
588
588
  /** Authorizations signed in this process, keyed by endpoint and requirement, kept until they expire. */
589
589
  const signingInFlight = new Map();
590
590
  const signedPayments = new Map();
591
+ /** The requirement and extensions a v2 server sends are echoed inside the payment header; nothing larger than this is signed. */
592
+ const MAX_PAYMENT_ECHO_BYTES = 32 * 1024;
593
+ /** A tools/call the client cancelled must not go on to sign: the SDK drops the result but leaves the handler running. */
594
+ function assertNotCancelled(signal, stage) {
595
+ if (signal?.aborted)
596
+ throw new Error(`pay_x402 was cancelled by the client before ${stage}; nothing more was signed or sent.`);
597
+ }
591
598
  function looksLikeTxHash(v) {
592
599
  return typeof v === 'string' && (/^0x[0-9a-fA-F]{64}$/.test(v) || /^[1-9A-HJ-NP-Za-km-z]{86,88}$/.test(v));
593
600
  }
@@ -827,7 +834,7 @@ function resolveChainId(network) {
827
834
  const AddressSchema = z.string().regex(/^(0x[a-fA-F0-9]{40}|[1-9A-HJ-NP-Za-km-z]{32,44})$/, 'Must be a 0x-prefixed 40-hex EVM address or a base58 Solana address');
828
835
  const server = new McpServer({
829
836
  name: 'agentwallet',
830
- version: '1.13.14',
837
+ version: '1.13.15',
831
838
  }, {
832
839
  instructions: `AgentWallet gives AI agents their own blockchain wallets. ${anyLocalMode()
833
840
  ? 'This server runs in LOCAL (self-custody) mode: the operator supplied the key, signing happens in this process, and the only spend guards are the environment caps (AGENTWALLET_MAX_TX_NATIVE / _TOKEN / _SOL per transaction, AGENTWALLET_MAX_AUTOPAY and AGENTWALLET_AUTOPAY_ASSETS for x402, AGENTWALLET_X402_MAX_TIMEOUT for authorization windows). There is no server-side pause, daily limit or approval email; pause_wallet, unpause_wallet and delete_wallet are refused.'
@@ -1515,7 +1522,8 @@ tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the server
1515
1522
  approval_id: z.string().optional().describe('An approval id from an earlier over-cap attempt. Once the owner has approved it, pass it here to make that one payment.'),
1516
1523
  fresh_authorization: z.boolean().optional().describe('Sign a new authorization even if one for the same endpoint, amount and recipient is still valid. By default a repeat call ' +
1517
1524
  'within the validity window re-sends the earlier signature (same nonce, so it cannot settle twice) instead of paying again.'),
1518
- }, async ({ url, wallet_id, method, headers: headersJson, body: reqBody, max_payment, prefer_chain, request_approval, approval_id, fresh_authorization }) => {
1525
+ }, async ({ url, wallet_id, method, headers: headersJson, body: reqBody, max_payment, prefer_chain, request_approval, approval_id, fresh_authorization }, extra) => {
1526
+ const cancel = extra?.signal;
1519
1527
  if (max_payment !== undefined && !/^\d+(\.\d+)?$/.test(String(max_payment).trim())) {
1520
1528
  throw new Error(`max_payment must be a decimal number of stablecoin units, got "${serverText(max_payment)}"; nothing was fetched.`);
1521
1529
  }
@@ -1668,7 +1676,9 @@ tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the server
1668
1676
  const METHOD = (method || 'GET').toUpperCase();
1669
1677
  const bodyDigest = createHash('sha256').update(METHOD !== 'GET' && reqBody ? reqBody : '').digest('hex').slice(0, 16);
1670
1678
  const walletKey = anyLocalMode() ? 'local' : String(wallet_id); // the local signer has one key whatever wallet_id says
1671
- const resourceTag = [walletKey, chainId, authorizationResource(url), METHOD, bodyDigest].join('|');
1679
+ // The chain is not part of the tag: a paywall rotating the chain per answer used
1680
+ // to harvest one cap-sized nonce per chain for a single purchase (round 8).
1681
+ const resourceTag = [walletKey, authorizationResource(url), METHOD, bodyDigest].join('|');
1672
1682
  const cacheKey = [walletKey, chainId, tokenAddress.toLowerCase(), option.payTo.toLowerCase(), rawAmount, option.scheme, resourceTag].join('|');
1673
1683
  for (const [k, v] of signedPayments)
1674
1684
  if (v.until <= Date.now())
@@ -1735,6 +1745,17 @@ tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the server
1735
1745
  // usage; the payer broadcasts nothing and pays no gas for either. AgentWallet's
1736
1746
  // own paywalls (verified by receipt), native-asset requests and Solana still use
1737
1747
  // a broadcast transfer proved by hash.
1748
+ // What the paid retry echoes back is bounded before anything is signed: a few MB
1749
+ // of junk in outputSchema or extensions used to become a 5 MB header kept in the
1750
+ // cache for the authorization's lifetime (round 8).
1751
+ const echoBytes = Buffer.byteLength(JSON.stringify({ accepted: option, resource: paymentInfo.resource, extensions: paymentInfo.extensions }));
1752
+ if (echoBytes > MAX_PAYMENT_ECHO_BYTES) {
1753
+ return jsonResponse({
1754
+ status: 402, payment_required: true, payment_made: false,
1755
+ error: `The endpoint's payment requirement is ${echoBytes} bytes once echoed into the payment header; nothing above ${MAX_PAYMENT_ECHO_BYTES} bytes is signed (no HTTP server accepts a header that size, so the payment could never be delivered).`,
1756
+ requested_amount: amount, token: tokenLabel, pay_to: option.payTo, chain_id: chainId,
1757
+ });
1758
+ }
1738
1759
  const standardExact = !isUpto && willSign;
1739
1760
  let headerName = 'X-PAYMENT';
1740
1761
  let paymentHeader = '';
@@ -1772,8 +1793,8 @@ tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the server
1772
1793
  const [, live] = other;
1773
1794
  return jsonResponse({
1774
1795
  status: 402, payment_required: true, payment_made: false,
1775
- error: `This resource already holds a live authorization signed ${Math.round((Date.now() - live.signedAt) / 1000)} s ago for ${live.amount} ${tokenLabel} (the endpoint's requirement changed between answers: amount, recipient, asset or scheme). It is re-sent by calling pay_x402 again with the same arguments once the endpoint asks for the same requirement. Nothing more is signed for this resource until that authorization expires.`,
1776
- live_authorization: { amount: live.amount, valid_until: new Date(live.until).toISOString(), payer: live.payer },
1796
+ error: `This resource already holds a live authorization signed ${Math.round((Date.now() - live.signedAt) / 1000)} s ago for ${live.amount} on chain ${live.chainId} (the endpoint's requirement changed between answers: amount, recipient, asset, scheme or chain). It is re-sent by calling pay_x402 again with the same arguments once the endpoint asks for the same requirement. Nothing more is signed for this resource until that authorization expires.`,
1797
+ live_authorization: { amount: live.amount, chain_id: live.chainId, valid_until: new Date(live.until).toISOString(), payer: live.payer },
1777
1798
  requested_amount: amount, token: tokenLabel, pay_to: option.payTo, chain_id: chainId,
1778
1799
  });
1779
1800
  }
@@ -1781,6 +1802,7 @@ tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the server
1781
1802
  let releaseSigning = () => { };
1782
1803
  let mineLock = null;
1783
1804
  if (!(cachedAuth && cachedAuth.until > Date.now())) {
1805
+ assertNotCancelled(cancel, 'signing');
1784
1806
  const mine = new Promise(r => { releaseSigning = r; });
1785
1807
  // Released in the finally below, whatever path this call takes. A timer
1786
1808
  // used to stand in for that and opened the gate under a slow signer, so
@@ -1842,16 +1864,18 @@ tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the server
1842
1864
  else {
1843
1865
  let txResult;
1844
1866
  if (isSolanaChain(chainId)) {
1867
+ // skip: a metered 402 from the API host on this send surfaces as the documented
1868
+ // error instead of an auto-payment hidden inside the pay_x402 result (round 8).
1845
1869
  txResult = (await api(`/wallets/${wallet_id}/send`, 'POST', tokenAddress
1846
1870
  ? { to: option.payTo, value: rawAmount, token_mint: tokenAddress, token_decimals: trustedDecimals, chain_id: chainId }
1847
- : { to: option.payTo, value: rawAmount, chain_id: chainId }));
1871
+ : { to: option.payTo, value: rawAmount, chain_id: chainId }, skip));
1848
1872
  }
1849
1873
  else if (tokenAddress) {
1850
1874
  const calldata = '0xa9059cbb' + padAddress(option.payTo) + encodeUint256(rawAmount);
1851
- txResult = (await api(`/wallets/${wallet_id}/send`, 'POST', { to: tokenAddress, value: '0', data: calldata, chain_id: chainId }));
1875
+ txResult = (await api(`/wallets/${wallet_id}/send`, 'POST', { to: tokenAddress, value: '0', data: calldata, chain_id: chainId }, skip));
1852
1876
  }
1853
1877
  else {
1854
- txResult = (await api(`/wallets/${wallet_id}/send`, 'POST', { to: option.payTo, value: rawAmount, data: '', chain_id: chainId }));
1878
+ txResult = (await api(`/wallets/${wallet_id}/send`, 'POST', { to: option.payTo, value: rawAmount, data: '', chain_id: chainId }, skip));
1855
1879
  }
1856
1880
  txHash = String(txResult.tx_hash || txResult.signature || '');
1857
1881
  if (!txHash)
@@ -1862,7 +1886,7 @@ tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the server
1862
1886
  const until = authorization ? Number(authorization.validBefore) * 1000
1863
1887
  : uptoAuth ? Number(uptoAuth.deadline) * 1000
1864
1888
  : Date.now() + maxAuthWindowSeconds() * 1000; // a broadcast transfer: the hash stays valid, re-sending it never pays twice
1865
- signedPayments.set(cacheKey, { headerName, paymentHeader, txHash, authorization, uptoAuth, payer, until, resource: resourceTag, amount, signedAt: Date.now() });
1889
+ signedPayments.set(cacheKey, { headerName, paymentHeader, txHash, authorization, uptoAuth, payer, until, resource: resourceTag, amount, signedAt: Date.now(), chainId });
1866
1890
  // A receipt on stderr the moment a signature exists, so a result the transport
1867
1891
  // drops (a client crash, an oversized frame) is still auditable (round 3).
1868
1892
  console.error(`[agentwallet] x402 authorization sent: ${amount} ${tokenLabel} to ${option.payTo} on chain ${chainId} for ${authorizationResource(url)} (${authorization ? 'nonce ' + authorization.nonce : uptoAuth ? 'permit2 nonce ' + uptoAuth.nonce : 'tx ' + txHash})`);
@@ -1875,11 +1899,14 @@ tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the server
1875
1899
  signingInFlight.delete(resourceLock);
1876
1900
  }
1877
1901
  // Step 6: Retry with the payment header and read the settlement receipt.
1902
+ // A cancellation here keeps the signed authorization cached for the next call
1903
+ // (same nonce, so it cannot pay twice) and hands nothing to the endpoint now.
1904
+ assertNotCancelled(cancel, 'the paid request');
1878
1905
  const retryHeaders = { ...reqHeaders, [headerName]: paymentHeader };
1879
1906
  const retryOptions = {
1880
1907
  method,
1881
1908
  headers: retryHeaders,
1882
- signal: AbortSignal.timeout(30_000),
1909
+ signal: cancel ? AbortSignal.any([AbortSignal.timeout(30_000), cancel]) : AbortSignal.timeout(30_000),
1883
1910
  };
1884
1911
  if (reqBody && method !== 'GET') {
1885
1912
  retryOptions.body = reqBody;
@@ -99,13 +99,28 @@ const DEFAULT_SOLANA_RPC = {
99
99
  901: 'https://api.devnet.solana.com',
100
100
  902: 'https://api.testnet.solana.com',
101
101
  };
102
+ /** A Solana RPC over plain http to anything but loopback exposes every query and lets an on-path host answer; refused like an EVM one (round 7). */
103
+ function assertSolanaScheme(url, name) {
104
+ let u;
105
+ try {
106
+ u = new URL(url);
107
+ }
108
+ catch {
109
+ throw new Error(`${name} is not a URL: "${url}".`);
110
+ }
111
+ const loopback = u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '[::1]';
112
+ if (u.protocol !== 'https:' && !(u.protocol === 'http:' && loopback)) {
113
+ throw new Error(`${name} must be https (got ${u.protocol}//${u.host}); plain http is allowed only for localhost.`);
114
+ }
115
+ return url;
116
+ }
102
117
  export function resolveSolanaRpc(chainId = 900) {
103
118
  const perChain = (process.env[`AGENTWALLET_SOLANA_RPC_${chainId}`] || '').trim();
104
119
  if (perChain)
105
- return perChain;
120
+ return assertSolanaScheme(perChain, `AGENTWALLET_SOLANA_RPC_${chainId}`);
106
121
  const explicit = (process.env.AGENTWALLET_SOLANA_RPC || '').trim();
107
122
  if (explicit)
108
- return explicit;
123
+ return assertSolanaScheme(explicit, 'AGENTWALLET_SOLANA_RPC');
109
124
  if (Object.keys(process.env).some(k => /^AGENTWALLET_SOLANA_RPC_\d+$/.test(k) && (process.env[k] || '').trim())) {
110
125
  throw new Error(`No Solana RPC for chain ${chainId}: AGENTWALLET_SOLANA_RPC_<chainId> is set for another cluster, so the public default is not used. Set AGENTWALLET_SOLANA_RPC_${chainId} or AGENTWALLET_SOLANA_RPC.`);
111
126
  }
@@ -152,6 +152,8 @@ export declare function generatePaymentId(): string;
152
152
  * info.id filled in. Other extensions pass through untouched.
153
153
  */
154
154
  export declare function withPaymentIdentifier(extensions?: Record<string, unknown>, id?: string): Record<string, unknown> | null;
155
+ /** Hard ceiling on the encoded payment header; pay_x402 refuses before signing well below it. */
156
+ export declare const MAX_PAYMENT_HEADER_BYTES: number;
155
157
  export declare function paymentHeaderFor(x402Version: number, payload: Record<string, unknown>): {
156
158
  name: string;
157
159
  value: string;
@@ -166,8 +166,13 @@ export function withPaymentIdentifier(extensions, id) {
166
166
  }
167
167
  return out;
168
168
  }
169
+ /** Hard ceiling on the encoded payment header; pay_x402 refuses before signing well below it. */
170
+ export const MAX_PAYMENT_HEADER_BYTES = 64 * 1024;
169
171
  export function paymentHeaderFor(x402Version, payload) {
170
172
  const value = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64');
173
+ if (value.length > MAX_PAYMENT_HEADER_BYTES) {
174
+ throw new Error(`x402: the payment header would be ${value.length} bytes; the server's requirement is too large to echo and the payment cannot be delivered.`);
175
+ }
171
176
  return { name: x402Version >= 2 ? 'PAYMENT-SIGNATURE' : 'X-PAYMENT', value };
172
177
  }
173
178
  function decodeBase64Json(v) {
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/hifriendbot/agentwallet-mcp.git"
7
7
  },
8
- "version": "1.13.14",
8
+ "version": "1.13.16",
9
9
  "description": "MCP server for AgentWallet: self-custody local signing on EVM and Solana, or hosted wallets. Sign, broadcast, pay x402 invoices, and accept x402 payments.",
10
10
  "type": "module",
11
11
  "main": "build/index.js",
@@ -20,7 +20,7 @@
20
20
  "scripts": {
21
21
  "build": "tsc",
22
22
  "start": "node build/index.js",
23
- "test": "node test/x402-payment.test.mjs && node test/eip3009.test.mjs && node test/permit2-risk.test.mjs && node test/token-cap.test.mjs && node test/spl-cap.test.mjs && node test/unknown-decimals-cap.test.mjs && node test/decimals-range-cap.test.mjs && node test/local-mode.test.mjs && node test/ssrf-guard.test.mjs && node test/x402-policy.test.mjs && node test/local-guard-shape.test.mjs && node test/allowance-total-cap.test.mjs && node test/autopay-scheme.test.mjs && node test/spl-recipient-guard.test.mjs && node test/approval-binding.test.mjs && node test/ata-rent-size.test.mjs && node test/final-url-guard.test.mjs && node test/audit-1135.test.mjs && node test/local-no-autopay.test.mjs && node test/audit-1136.test.mjs && node test/delete-confirm.test.mjs && node test/audit-1138.test.mjs && node test/audit-1139.test.mjs && node test/audit-11310.test.mjs && node test/audit-11311.test.mjs && node test/audit-11312.test.mjs && node test/audit-11313.test.mjs",
23
+ "test": "node test/x402-payment.test.mjs && node test/eip3009.test.mjs && node test/permit2-risk.test.mjs && node test/token-cap.test.mjs && node test/spl-cap.test.mjs && node test/unknown-decimals-cap.test.mjs && node test/decimals-range-cap.test.mjs && node test/local-mode.test.mjs && node test/ssrf-guard.test.mjs && node test/x402-policy.test.mjs && node test/local-guard-shape.test.mjs && node test/allowance-total-cap.test.mjs && node test/autopay-scheme.test.mjs && node test/spl-recipient-guard.test.mjs && node test/approval-binding.test.mjs && node test/ata-rent-size.test.mjs && node test/final-url-guard.test.mjs && node test/audit-1135.test.mjs && node test/local-no-autopay.test.mjs && node test/audit-1136.test.mjs && node test/delete-confirm.test.mjs && node test/audit-1138.test.mjs && node test/audit-1139.test.mjs && node test/audit-11310.test.mjs && node test/audit-11311.test.mjs && node test/audit-11312.test.mjs && node test/audit-11313.test.mjs && node test/audit-11316.test.mjs",
24
24
  "prepublishOnly": "npm run build"
25
25
  },
26
26
  "keywords": [