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 +37 -10
- package/build/local-solana.js +17 -2
- package/build/x402-eip3009.d.ts +2 -0
- package/build/x402-eip3009.js +5 -0
- package/package.json +2 -2
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.
|
|
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
|
-
|
|
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} ${
|
|
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;
|
package/build/local-solana.js
CHANGED
|
@@ -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
|
}
|
package/build/x402-eip3009.d.ts
CHANGED
|
@@ -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;
|
package/build/x402-eip3009.js
CHANGED
|
@@ -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.
|
|
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": [
|