agentwallet-mcp 1.13.8 → 1.13.9

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
@@ -20,7 +20,7 @@ import { localSignAuthorization, localEthCall, localGetCode, localSignPermit2Upt
20
20
  import { WRAPPED_NATIVE } from './wrapped-native.js';
21
21
  import { buildUptoAuthorization, uptoPayload, permit2AllowanceCalldata, permit2ApproveCalldata, decodeUint, permit2Address, } from './x402-permit2.js';
22
22
  import { assessTokenRisk } from './token-risk.js';
23
- import { isLegacyAgentWalletAccept, requiredAmount, buildAuthorization, buildPaymentPayload, buildPaymentPayloadRaw, paymentHeaderFor, parsePaymentRequired, parseSettlement, pickOption, knownTokenDomain, parseEip7702Delegation, erc1271ProbeCalldata, classifyErc1271Probe, settlementVerdict, legacyReceiptAllowed, } from './x402-eip3009.js';
23
+ import { isLegacyAgentWalletAccept, requiredAmount, buildAuthorization, buildPaymentPayload, buildPaymentPayloadRaw, paymentHeaderFor, parsePaymentRequired, parseSettlement, pickOption, knownTokenDomain, decodeAbiString as decodeDomainString, parseEip7702Delegation, erc1271ProbeCalldata, classifyErc1271Probe, settlementVerdict, legacyReceiptAllowed, } from './x402-eip3009.js';
24
24
  import { isSolanaLocalMode, getSolanaAddress, solanaWalletRecord, localSolBalance, localSplBalance, localSolTransfer, localSplTransfer, resolveSolanaRpc, mintDecimalsStrict, } from './local-solana.js';
25
25
  /** True when either chain family is running non-custodially. */
26
26
  function anyLocalMode() {
@@ -679,7 +679,7 @@ function resolveChainId(network) {
679
679
  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');
680
680
  const server = new McpServer({
681
681
  name: 'agentwallet',
682
- version: '1.13.8',
682
+ version: '1.13.9',
683
683
  }, {
684
684
  instructions: `AgentWallet gives AI agents their own blockchain wallets. ${anyLocalMode()
685
685
  ? '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 AGENTWALLET_MAX_TX_* environment caps. There is no server-side pause, daily limit or approval email; pause_wallet, unpause_wallet and delete_wallet are refused.'
@@ -1290,8 +1290,11 @@ async function resolveTokenDomain(chainId, asset, extra) {
1290
1290
  return declared;
1291
1291
  let name = '', version = '';
1292
1292
  try {
1293
- name = decodeAbiString(await ethCallHex(chainId, asset, '0x06fdde03')); // name()
1294
- version = decodeAbiString(await ethCallHex(chainId, asset, '0x54fd4d50')); // version()
1293
+ // The signing decoder (256 bytes, strict UTF-8), not the 64-char display sanitiser
1294
+ // get_token_info uses: a truncated or replaced name signs under a domain the
1295
+ // token never had (round 3).
1296
+ name = decodeDomainString(await ethCallHex(chainId, asset, '0x06fdde03')); // name()
1297
+ version = decodeDomainString(await ethCallHex(chainId, asset, '0x54fd4d50')); // version()
1295
1298
  }
1296
1299
  catch { /* refused below */ }
1297
1300
  if (!name || !version) {
@@ -1609,7 +1612,7 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
1609
1612
  const [, live] = other;
1610
1613
  return jsonResponse({
1611
1614
  status: 402, payment_required: true, payment_made: false,
1612
- 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; to sign a second authorization for this resource pass fresh_authorization=true.`,
1615
+ 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.`,
1613
1616
  live_authorization: { amount: live.amount, valid_until: new Date(live.until).toISOString(), payer: live.payer },
1614
1617
  requested_amount: amount, token: tokenLabel, pay_to: option.payTo, chain_id: chainId,
1615
1618
  });
@@ -1654,7 +1657,7 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
1654
1657
  token: tokenLabel, token_address: tokenAddress, chain_id: chainId, payer, permit2: permit2Address(chainId),
1655
1658
  error: `This endpoint uses the x402 "upto" scheme, which settles through Permit2. Wallet ${payer} has not approved ` +
1656
1659
  `${tokenLabel} to Permit2 on chain ${chainId}. Call approve_permit2 once for this token (a normal transaction that needs gas), ` +
1657
- `or set AGENTWALLET_PERMIT2_AUTO_APPROVE=1, then call pay_x402 again.`,
1660
+ `then call pay_x402 again.` + (assetPolicy.via === 'stablecoin' ? ' The operator may instead set AGENTWALLET_PERMIT2_AUTO_APPROVE=1 (registry stablecoins only); that is the operator\'s setting, not the agent\'s.' : ''),
1658
1661
  });
1659
1662
  }
1660
1663
  }
@@ -1749,6 +1752,8 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
1749
1752
  const retryServedFrom = finalUrlOf(retryRes, url);
1750
1753
  const retryCrossOrigin = new URL(retryServedFrom).origin !== new URL(url).origin;
1751
1754
  const verdict = settlementVerdict(retryRes.status, settlement, retryCrossOrigin);
1755
+ const claimedRaw = typeof settlement?.amount === 'string' && /^\d+$/.test(settlement.amount) ? settlement.amount : null;
1756
+ const claimedAboveMax = claimedRaw !== null && BigInt(claimedRaw) > BigInt(rawAmount);
1752
1757
  if (retryCrossOrigin) {
1753
1758
  retryError = `The paid request was redirected to another origin (${new URL(retryServedFrom).origin}); the payment header is never forwarded across origins, so this response did not see the payment.`;
1754
1759
  }
@@ -1773,6 +1778,9 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
1773
1778
  permit2_approval_tx: permit2ApprovalTx,
1774
1779
  x402_version: x402Version,
1775
1780
  amount,
1781
+ // For upto the amount is the MAXIMUM the facilitator may settle, not what was
1782
+ // taken; the server's claimed settled amount is shown apart and unverified (round 3).
1783
+ ...(isUpto ? { amount_is_maximum: true, authorized_max: amount, settled_amount_claimed: serverText(settlement?.amount), settled_claim_inconsistent: claimedAboveMax } : {}),
1776
1784
  token: tokenLabel,
1777
1785
  token_address: tokenAddress || null,
1778
1786
  network: option.network,
@@ -121,7 +121,14 @@ export function decodeAbiString(hex) {
121
121
  if (!Number.isFinite(len) || len <= 0 || len > 256)
122
122
  return '';
123
123
  const bytes = Buffer.from(h.slice(128, 128 + len * 2), 'hex');
124
- return bytes.toString('utf8');
124
+ // The domain separator is keccak of the raw bytes: a name that is not valid
125
+ // UTF-8 cannot be reproduced from a string, so it is refused, never guessed.
126
+ try {
127
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
128
+ }
129
+ catch {
130
+ return '';
131
+ }
125
132
  }
126
133
  /** Build the payment payload the resource server expects for this x402 version. */
127
134
  export function buildPaymentPayload(x402Version, req, auth, signature, resource, extensions) {
@@ -188,7 +195,11 @@ export function parsePaymentRequired(getHeader, body) {
188
195
  network and payTo are strings. A null entry, or an array where an address
189
196
  should be, used to reach the pickers and surface a raw TypeError or a
190
197
  coerced value (2026-10-04 round 2). Numeric amounts are kept as strings. */
191
- const MAX_ACCEPTS = 64; // a bound on the picker's work, well above anything a real server lists
198
+ const MAX_ACCEPTS = 64;
199
+ /** An all-uppercase hex address is not an EIP-55 checksum and viem refuses it at signing; lowercase has no checksum to fail (round 3). Mixed case is kept: it IS a checksum. */
200
+ function lowerIfAllCaps(v) {
201
+ return (typeof v === 'string' && /^0x[0-9A-F]{40}$/.test(v) && /[A-F]/.test(v)) ? v.toLowerCase() : v;
202
+ } // a bound on the picker's work, well above anything a real server lists
192
203
  export function sanitizeRequired(req) {
193
204
  const str = (v) => (typeof v === 'string' ? v : typeof v === 'number' && Number.isFinite(v) ? String(v) : undefined);
194
205
  const accepts = [];
@@ -196,7 +207,7 @@ export function sanitizeRequired(req) {
196
207
  if (!a || typeof a !== 'object' || Array.isArray(a))
197
208
  continue;
198
209
  const o = a;
199
- const scheme = str(o.scheme), network = str(o.network), payTo = str(o.payTo);
210
+ const scheme = str(o.scheme), network = str(o.network), payTo = lowerIfAllCaps(str(o.payTo));
200
211
  if (scheme === undefined || network === undefined || payTo === undefined)
201
212
  continue;
202
213
  const extraIn = (o.extra && typeof o.extra === 'object' && !Array.isArray(o.extra)) ? o.extra : undefined;
@@ -206,6 +217,8 @@ export function sanitizeRequired(req) {
206
217
  if (k in extra && typeof extra[k] !== 'string')
207
218
  delete extra[k];
208
219
  }
220
+ if (extra && typeof extra.token === 'string')
221
+ extra.token = lowerIfAllCaps(extra.token);
209
222
  const out = { ...o, scheme, network, payTo };
210
223
  for (const k of ['amount', 'maxAmountRequired', 'asset', 'description', 'resource', 'mimeType']) {
211
224
  const v = str(o[k]);
@@ -214,6 +227,8 @@ export function sanitizeRequired(req) {
214
227
  else
215
228
  out[k] = v;
216
229
  }
230
+ if (typeof out.asset === 'string')
231
+ out.asset = lowerIfAllCaps(out.asset);
217
232
  if (extra)
218
233
  out.extra = extra;
219
234
  else
@@ -267,7 +282,11 @@ export function pickOption(accepts, resolveChainId, preferChain) {
267
282
  const payable = [...exact, ...upto];
268
283
  if (payable.length === 0) {
269
284
  const schemes = Array.from(new Set(accepts.map(a => String(a.scheme).slice(0, 40)))).slice(0, 10).join(', ') || 'none';
270
- const reason = accepts.some(a => a.scheme === 'upto')
285
+ if (onChain.length === 0 && accepts.length > 0) {
286
+ const nets = Array.from(new Set(accepts.map(a => String(a.network).slice(0, 40)))).slice(0, 10).join(', ');
287
+ return { option: null, chainId: null, reason: `No payable option: the offered network(s) ${nets} are not ones this client knows (pass a CAIP-2 id like eip155:8453 or a chain id).` };
288
+ }
289
+ const reason = onChain.some(a => a.scheme === 'upto')
271
290
  ? 'This endpoint offers the x402 "upto" scheme without an extra.facilitatorAddress (or without an asset), so the ' +
272
291
  'Permit2 authorization cannot be bound to a facilitator. AgentWallet will not approximate it with an upfront transfer. ' +
273
292
  'Ask the endpoint operator to publish facilitatorAddress, or an "exact" option.'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agentwallet-mcp",
3
3
  "mcpName": "io.github.hifriendbot/agentwallet",
4
- "version": "1.13.8",
4
+ "version": "1.13.9",
5
5
  "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.",
6
6
  "type": "module",
7
7
  "main": "build/index.js",
@@ -16,7 +16,7 @@
16
16
  "scripts": {
17
17
  "build": "tsc",
18
18
  "start": "node build/index.js",
19
- "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",
19
+ "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",
20
20
  "prepublishOnly": "npm run build"
21
21
  },
22
22
  "keywords": [