agentwallet-mcp 1.10.0 → 1.10.2
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 +328 -316
- package/build/index.js +149 -96
- package/build/local-wallet.d.ts +0 -4
- package/build/local-wallet.js +46 -0
- package/build/ssrf-guard.d.ts +38 -0
- package/build/ssrf-guard.js +178 -0
- package/build/x402-payment.d.ts +43 -7
- package/build/x402-payment.js +100 -11
- package/package.json +68 -68
package/build/x402-payment.d.ts
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Isolated from the server bootstrap in index.ts so the funds-sensitive amount
|
|
5
5
|
* math can be unit-tested without starting the MCP server.
|
|
6
|
+
*
|
|
7
|
+
* SECURITY INVARIANT: token decimals must NEVER come from the 402 response.
|
|
8
|
+
* A resource server controls `asset`, `payTo`, `maxAmountRequired` AND
|
|
9
|
+
* `requiredDecimals`. If the cap is scaled using server-declared decimals, the
|
|
10
|
+
* server can inflate the cap arbitrarily: declaring 18 decimals for 6-decimal
|
|
11
|
+
* USDC turns a "1 USDC" cap into 10^18 base units, so a request for
|
|
12
|
+
* 1,000,000,000,000 units (1,000,000 USDC) passes a 1 USDC cap. Reported by
|
|
13
|
+
* ARC Security Research, 2026-08-02, against 1.10.1. Decimals are now resolved
|
|
14
|
+
* from a trusted source and the declared value is only ever used to detect a
|
|
15
|
+
* mismatch and refuse.
|
|
6
16
|
*/
|
|
7
17
|
export interface X402AcceptLike {
|
|
8
18
|
maxAmountRequired: string;
|
|
@@ -12,22 +22,48 @@ export interface X402AcceptLike {
|
|
|
12
22
|
token?: string;
|
|
13
23
|
};
|
|
14
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Decimals for assets that x402 endpoints actually settle in, keyed by
|
|
27
|
+
* chain id then lowercased contract address (or SPL mint on Solana).
|
|
28
|
+
*
|
|
29
|
+
* This exists so the common path needs no network call, and so local
|
|
30
|
+
* self-custody mode (which may have no reachable API) can still verify a cap.
|
|
31
|
+
* It is intentionally small: every entry is a value that can be checked by
|
|
32
|
+
* hand. Anything not listed is resolved on-chain instead, and if that fails
|
|
33
|
+
* the payment is refused rather than guessed.
|
|
34
|
+
*/
|
|
35
|
+
export declare const TRUSTED_DECIMALS: Record<number, Record<string, number>>;
|
|
36
|
+
/** Solana SPL mints, keyed by mint address (case-sensitive base58). */
|
|
37
|
+
export declare const TRUSTED_SPL_DECIMALS: Record<string, number>;
|
|
38
|
+
/**
|
|
39
|
+
* Look up decimals we already know to be correct. Returns null when the asset
|
|
40
|
+
* is unknown, which means the caller must resolve it on-chain or refuse.
|
|
41
|
+
*/
|
|
42
|
+
export declare function lookupTrustedDecimals(chainId: number, token: string): number | null;
|
|
43
|
+
/**
|
|
44
|
+
* Guard the server's declared decimals against the trusted value.
|
|
45
|
+
*
|
|
46
|
+
* A mismatch is not a formatting quirk. It is the exact shape of the cap-bypass
|
|
47
|
+
* attack, so it fails closed and says so.
|
|
48
|
+
*/
|
|
49
|
+
export declare function assertDeclaredDecimals(declared: unknown, trusted: number): void;
|
|
15
50
|
/**
|
|
16
51
|
* Derive the on-chain transfer parameters from an x402 payment requirement.
|
|
17
52
|
*
|
|
18
53
|
* CRITICAL: x402 `maxAmountRequired` is ALREADY denominated in base/atomic
|
|
19
54
|
* units (e.g. "10000" = 0.01 USDC at 6 decimals). It MUST be used directly.
|
|
20
55
|
* Running it through parseUnits() re-scales it by 10**decimals and overpays by
|
|
21
|
-
* that factor (1,000,000x for USDC).
|
|
22
|
-
* always used maxAmountRequired directly.
|
|
56
|
+
* that factor (1,000,000x for USDC).
|
|
23
57
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
58
|
+
* Enforces a hard ceiling so a malformed or malicious 402 response can never
|
|
59
|
+
* authorize a transfer larger than `maxAutopayHuman` units of the asset.
|
|
26
60
|
*
|
|
27
61
|
* @param accept the chosen x402 accept option
|
|
28
62
|
* @param maxAutopayHuman max auto-pay, in human-readable units of the asset
|
|
63
|
+
* @param trustedDecimals decimals resolved from a registry or the token
|
|
64
|
+
* contract. MUST NOT be taken from the 402 response.
|
|
29
65
|
*/
|
|
30
|
-
export declare function deriveX402Payment(accept: X402AcceptLike, maxAutopayHuman: string): {
|
|
66
|
+
export declare function deriveX402Payment(accept: X402AcceptLike, maxAutopayHuman: string, trustedDecimals: number): {
|
|
31
67
|
tokenAddress: string;
|
|
32
68
|
rawAmount: string;
|
|
33
69
|
decimals: number;
|
|
@@ -42,7 +78,7 @@ export declare function deriveX402Payment(accept: X402AcceptLike, maxAutopayHuma
|
|
|
42
78
|
* can return a structured rejection to the calling agent.
|
|
43
79
|
*
|
|
44
80
|
* @param rawAmountRequired x402 maxAmountRequired, already in base units
|
|
45
|
-
* @param
|
|
81
|
+
* @param trustedDecimals token decimals from a trusted source, NOT the 402 body
|
|
46
82
|
* @param capHuman cap in human-readable units of the asset (e.g. "1")
|
|
47
83
|
*/
|
|
48
|
-
export declare function isWithinCap(rawAmountRequired: string,
|
|
84
|
+
export declare function isWithinCap(rawAmountRequired: string, trustedDecimals: number, capHuman: string): boolean;
|
package/build/x402-payment.js
CHANGED
|
@@ -3,12 +3,100 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Isolated from the server bootstrap in index.ts so the funds-sensitive amount
|
|
5
5
|
* math can be unit-tested without starting the MCP server.
|
|
6
|
+
*
|
|
7
|
+
* SECURITY INVARIANT: token decimals must NEVER come from the 402 response.
|
|
8
|
+
* A resource server controls `asset`, `payTo`, `maxAmountRequired` AND
|
|
9
|
+
* `requiredDecimals`. If the cap is scaled using server-declared decimals, the
|
|
10
|
+
* server can inflate the cap arbitrarily: declaring 18 decimals for 6-decimal
|
|
11
|
+
* USDC turns a "1 USDC" cap into 10^18 base units, so a request for
|
|
12
|
+
* 1,000,000,000,000 units (1,000,000 USDC) passes a 1 USDC cap. Reported by
|
|
13
|
+
* ARC Security Research, 2026-08-02, against 1.10.1. Decimals are now resolved
|
|
14
|
+
* from a trusted source and the declared value is only ever used to detect a
|
|
15
|
+
* mismatch and refuse.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Decimals for assets that x402 endpoints actually settle in, keyed by
|
|
19
|
+
* chain id then lowercased contract address (or SPL mint on Solana).
|
|
20
|
+
*
|
|
21
|
+
* This exists so the common path needs no network call, and so local
|
|
22
|
+
* self-custody mode (which may have no reachable API) can still verify a cap.
|
|
23
|
+
* It is intentionally small: every entry is a value that can be checked by
|
|
24
|
+
* hand. Anything not listed is resolved on-chain instead, and if that fails
|
|
25
|
+
* the payment is refused rather than guessed.
|
|
26
|
+
*/
|
|
27
|
+
export const TRUSTED_DECIMALS = {
|
|
28
|
+
// Ethereum
|
|
29
|
+
1: {
|
|
30
|
+
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': 6, // USDC
|
|
31
|
+
'0xdac17f958d2ee523a2206206994597c13d831ec7': 6, // USDT
|
|
32
|
+
'0x6b175474e89094c44da98b954eedeac495271d0f': 18, // DAI
|
|
33
|
+
},
|
|
34
|
+
// Base
|
|
35
|
+
8453: {
|
|
36
|
+
'0x833589fcd6edb6e08f4c7c32d4f71b54bda02913': 6, // USDC
|
|
37
|
+
'0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca': 6, // USDbC
|
|
38
|
+
'0x4200000000000000000000000000000000000006': 18, // WETH
|
|
39
|
+
},
|
|
40
|
+
// Polygon
|
|
41
|
+
137: {
|
|
42
|
+
'0x3c499c542cef5e3811e1192ce70d8cc03d5c3359': 6, // USDC (native)
|
|
43
|
+
'0xc2132d05d31c914a87c6611c10748aeb04b58e8f': 6, // USDT
|
|
44
|
+
},
|
|
45
|
+
// Arbitrum One
|
|
46
|
+
42161: {
|
|
47
|
+
'0xaf88d065e77c8cc2239327c5edb3a432268e5831': 6, // USDC
|
|
48
|
+
},
|
|
49
|
+
// Optimism
|
|
50
|
+
10: {
|
|
51
|
+
'0x0b2c639c533813f4aa9d7837caf62653d097ff85': 6, // USDC
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
/** Solana SPL mints, keyed by mint address (case-sensitive base58). */
|
|
55
|
+
export const TRUSTED_SPL_DECIMALS = {
|
|
56
|
+
EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: 6, // USDC
|
|
57
|
+
Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB: 6, // USDT
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Look up decimals we already know to be correct. Returns null when the asset
|
|
61
|
+
* is unknown, which means the caller must resolve it on-chain or refuse.
|
|
62
|
+
*/
|
|
63
|
+
export function lookupTrustedDecimals(chainId, token) {
|
|
64
|
+
if (!token)
|
|
65
|
+
return null;
|
|
66
|
+
const spl = TRUSTED_SPL_DECIMALS[token];
|
|
67
|
+
if (typeof spl === 'number')
|
|
68
|
+
return spl;
|
|
69
|
+
const forChain = TRUSTED_DECIMALS[chainId];
|
|
70
|
+
if (!forChain)
|
|
71
|
+
return null;
|
|
72
|
+
const d = forChain[token.toLowerCase()];
|
|
73
|
+
return typeof d === 'number' ? d : null;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Guard the server's declared decimals against the trusted value.
|
|
77
|
+
*
|
|
78
|
+
* A mismatch is not a formatting quirk. It is the exact shape of the cap-bypass
|
|
79
|
+
* attack, so it fails closed and says so.
|
|
6
80
|
*/
|
|
81
|
+
export function assertDeclaredDecimals(declared, trusted) {
|
|
82
|
+
if (declared === undefined || declared === null)
|
|
83
|
+
return; // absent is fine, we use trusted
|
|
84
|
+
if (typeof declared !== 'number' || !Number.isInteger(declared) || declared < 0 || declared > 36) {
|
|
85
|
+
throw new Error(`x402: malformed requiredDecimals (${String(declared)}). Refusing to derive a payment amount.`);
|
|
86
|
+
}
|
|
87
|
+
if (declared !== trusted) {
|
|
88
|
+
throw new Error(`x402 blocked: the endpoint declared ${declared} decimals for this asset but its ` +
|
|
89
|
+
`on-chain value is ${trusted}. This is how a payment cap is bypassed, so the payment was refused.`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
7
92
|
/** Convert a human-readable decimal amount (e.g. "0.01") to base/atomic units. */
|
|
8
93
|
function toBaseUnits(amount, decimals) {
|
|
9
94
|
if (!/^\d+(\.\d+)?$/.test(amount)) {
|
|
10
95
|
throw new Error(`Invalid amount "${amount}".`);
|
|
11
96
|
}
|
|
97
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 36) {
|
|
98
|
+
throw new Error(`Invalid decimals "${decimals}".`);
|
|
99
|
+
}
|
|
12
100
|
const [whole, frac = ''] = amount.split('.');
|
|
13
101
|
const fracPadded = frac.slice(0, decimals).padEnd(decimals, '0');
|
|
14
102
|
return BigInt(whole + fracPadded).toString();
|
|
@@ -19,30 +107,31 @@ function toBaseUnits(amount, decimals) {
|
|
|
19
107
|
* CRITICAL: x402 `maxAmountRequired` is ALREADY denominated in base/atomic
|
|
20
108
|
* units (e.g. "10000" = 0.01 USDC at 6 decimals). It MUST be used directly.
|
|
21
109
|
* Running it through parseUnits() re-scales it by 10**decimals and overpays by
|
|
22
|
-
* that factor (1,000,000x for USDC).
|
|
23
|
-
* always used maxAmountRequired directly.
|
|
110
|
+
* that factor (1,000,000x for USDC).
|
|
24
111
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
112
|
+
* Enforces a hard ceiling so a malformed or malicious 402 response can never
|
|
113
|
+
* authorize a transfer larger than `maxAutopayHuman` units of the asset.
|
|
27
114
|
*
|
|
28
115
|
* @param accept the chosen x402 accept option
|
|
29
116
|
* @param maxAutopayHuman max auto-pay, in human-readable units of the asset
|
|
117
|
+
* @param trustedDecimals decimals resolved from a registry or the token
|
|
118
|
+
* contract. MUST NOT be taken from the 402 response.
|
|
30
119
|
*/
|
|
31
|
-
export function deriveX402Payment(accept, maxAutopayHuman) {
|
|
32
|
-
const decimals = accept.requiredDecimals || 6;
|
|
120
|
+
export function deriveX402Payment(accept, maxAutopayHuman, trustedDecimals) {
|
|
33
121
|
// Standard x402 uses `asset` for the token address; AgentWallet's own server
|
|
34
122
|
// currently emits it under extra.token. Prefer the standard field, fall back.
|
|
35
123
|
const tokenAddress = accept.asset || accept.extra?.token || '';
|
|
124
|
+
assertDeclaredDecimals(accept.requiredDecimals, trustedDecimals);
|
|
36
125
|
const rawAmount = accept.maxAmountRequired;
|
|
37
126
|
if (!/^\d+$/.test(rawAmount)) {
|
|
38
127
|
throw new Error(`x402 auto-pay: invalid maxAmountRequired "${rawAmount}" (expected an integer base-unit amount).`);
|
|
39
128
|
}
|
|
40
|
-
const capRaw = toBaseUnits(maxAutopayHuman,
|
|
129
|
+
const capRaw = toBaseUnits(maxAutopayHuman, trustedDecimals);
|
|
41
130
|
if (BigInt(rawAmount) > BigInt(capRaw)) {
|
|
42
131
|
throw new Error(`x402 auto-pay blocked: required amount (${rawAmount} base units) exceeds the ` +
|
|
43
132
|
`AGENTWALLET_MAX_AUTOPAY cap of ${maxAutopayHuman}. Raise AGENTWALLET_MAX_AUTOPAY to allow it.`);
|
|
44
133
|
}
|
|
45
|
-
return { tokenAddress, rawAmount, decimals };
|
|
134
|
+
return { tokenAddress, rawAmount, decimals: trustedDecimals };
|
|
46
135
|
}
|
|
47
136
|
/**
|
|
48
137
|
* Returns true if an x402 required amount (already in base/atomic units) is
|
|
@@ -54,13 +143,13 @@ export function deriveX402Payment(accept, maxAutopayHuman) {
|
|
|
54
143
|
* can return a structured rejection to the calling agent.
|
|
55
144
|
*
|
|
56
145
|
* @param rawAmountRequired x402 maxAmountRequired, already in base units
|
|
57
|
-
* @param
|
|
146
|
+
* @param trustedDecimals token decimals from a trusted source, NOT the 402 body
|
|
58
147
|
* @param capHuman cap in human-readable units of the asset (e.g. "1")
|
|
59
148
|
*/
|
|
60
|
-
export function isWithinCap(rawAmountRequired,
|
|
149
|
+
export function isWithinCap(rawAmountRequired, trustedDecimals, capHuman) {
|
|
61
150
|
if (!/^\d+$/.test(rawAmountRequired)) {
|
|
62
151
|
throw new Error(`Invalid maxAmountRequired "${rawAmountRequired}" (expected an integer base-unit amount).`);
|
|
63
152
|
}
|
|
64
|
-
const capRaw = toBaseUnits(capHuman,
|
|
153
|
+
const capRaw = toBaseUnits(capHuman, trustedDecimals);
|
|
65
154
|
return BigInt(rawAmountRequired) <= BigInt(capRaw);
|
|
66
155
|
}
|
package/package.json
CHANGED
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "agentwallet-mcp",
|
|
3
|
-
"mcpName": "io.github.hifriendbot/agentwallet",
|
|
4
|
-
"version": "1.10.
|
|
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
|
-
"type": "module",
|
|
7
|
-
"main": "build/index.js",
|
|
8
|
-
"bin": {
|
|
9
|
-
"agentwallet-mcp": "build/index.js"
|
|
10
|
-
},
|
|
11
|
-
"files": [
|
|
12
|
-
"build",
|
|
13
|
-
"README.md",
|
|
14
|
-
"LICENSE"
|
|
15
|
-
],
|
|
16
|
-
"scripts": {
|
|
17
|
-
"build": "tsc",
|
|
18
|
-
"start": "node build/index.js",
|
|
19
|
-
"test": "node test/x402-payment.test.mjs && node test/local-mode.test.mjs",
|
|
20
|
-
"prepublishOnly": "npm run build"
|
|
21
|
-
},
|
|
22
|
-
"keywords": [
|
|
23
|
-
"mcp",
|
|
24
|
-
"model-context-protocol",
|
|
25
|
-
"wallet",
|
|
26
|
-
"ai-agent",
|
|
27
|
-
"blockchain",
|
|
28
|
-
"evm",
|
|
29
|
-
"ethereum",
|
|
30
|
-
"base",
|
|
31
|
-
"arbitrum",
|
|
32
|
-
"optimism",
|
|
33
|
-
"polygon",
|
|
34
|
-
"avalanche",
|
|
35
|
-
"bsc",
|
|
36
|
-
"zora",
|
|
37
|
-
"pulsechain",
|
|
38
|
-
"solana",
|
|
39
|
-
"spl-token",
|
|
40
|
-
"erc20",
|
|
41
|
-
"token-transfer",
|
|
42
|
-
"transaction-signing",
|
|
43
|
-
"x402",
|
|
44
|
-
"http-402",
|
|
45
|
-
"agent-payments",
|
|
46
|
-
"agentwallet",
|
|
47
|
-
"self-custody",
|
|
48
|
-
"non-custodial",
|
|
49
|
-
"local-signing"
|
|
50
|
-
],
|
|
51
|
-
"author": "HiFriendbot (https://hifriendbot.com)",
|
|
52
|
-
"license": "MIT",
|
|
53
|
-
"homepage": "https://hifriendbot.com/wallet/",
|
|
54
|
-
"engines": {
|
|
55
|
-
"node": ">=18.0.0"
|
|
56
|
-
},
|
|
57
|
-
"dependencies": {
|
|
58
|
-
"@modelcontextprotocol/sdk": "^1.11.4",
|
|
59
|
-
"@solana/web3.js": "^1.98.4",
|
|
60
|
-
"bs58": "^6.0.0",
|
|
61
|
-
"viem": "^2.55.10",
|
|
62
|
-
"zod": "^3.24.0"
|
|
63
|
-
},
|
|
64
|
-
"devDependencies": {
|
|
65
|
-
"@types/node": "^20.0.0",
|
|
66
|
-
"typescript": "^5.3.0"
|
|
67
|
-
}
|
|
68
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "agentwallet-mcp",
|
|
3
|
+
"mcpName": "io.github.hifriendbot/agentwallet",
|
|
4
|
+
"version": "1.10.2",
|
|
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
|
+
"type": "module",
|
|
7
|
+
"main": "build/index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"agentwallet-mcp": "build/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"build",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"start": "node build/index.js",
|
|
19
|
+
"test": "node test/x402-payment.test.mjs && node test/token-cap.test.mjs && node test/local-mode.test.mjs && node test/ssrf-guard.test.mjs",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"mcp",
|
|
24
|
+
"model-context-protocol",
|
|
25
|
+
"wallet",
|
|
26
|
+
"ai-agent",
|
|
27
|
+
"blockchain",
|
|
28
|
+
"evm",
|
|
29
|
+
"ethereum",
|
|
30
|
+
"base",
|
|
31
|
+
"arbitrum",
|
|
32
|
+
"optimism",
|
|
33
|
+
"polygon",
|
|
34
|
+
"avalanche",
|
|
35
|
+
"bsc",
|
|
36
|
+
"zora",
|
|
37
|
+
"pulsechain",
|
|
38
|
+
"solana",
|
|
39
|
+
"spl-token",
|
|
40
|
+
"erc20",
|
|
41
|
+
"token-transfer",
|
|
42
|
+
"transaction-signing",
|
|
43
|
+
"x402",
|
|
44
|
+
"http-402",
|
|
45
|
+
"agent-payments",
|
|
46
|
+
"agentwallet",
|
|
47
|
+
"self-custody",
|
|
48
|
+
"non-custodial",
|
|
49
|
+
"local-signing"
|
|
50
|
+
],
|
|
51
|
+
"author": "HiFriendbot (https://hifriendbot.com)",
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"homepage": "https://hifriendbot.com/wallet/",
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=18.0.0"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@modelcontextprotocol/sdk": "^1.11.4",
|
|
59
|
+
"@solana/web3.js": "^1.98.4",
|
|
60
|
+
"bs58": "^6.0.0",
|
|
61
|
+
"viem": "^2.55.10",
|
|
62
|
+
"zod": "^3.24.0"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@types/node": "^20.0.0",
|
|
66
|
+
"typescript": "^5.3.0"
|
|
67
|
+
}
|
|
68
|
+
}
|