agentwallet-mcp 1.13.1 → 1.13.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 +1 -1
- package/build/index.js +10 -9
- package/build/local-solana.js +21 -4
- package/build/x402-payment.d.ts +32 -0
- package/build/x402-payment.js +34 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -245,7 +245,7 @@ pay_x402(
|
|
|
245
245
|
|
|
246
246
|
**Schemes.** `exact` (EIP-3009, gasless for the payer) is preferred and works on every EVM chain AgentWallet knows. `upto` (a Permit2 maximum the seller settles at actual usage) is signed as a `PermitWitnessTransferFrom` bound to the endpoint's facilitator; it needs a one-time `approve_permit2` per token (or `AGENTWALLET_PERMIT2_AUTO_APPROVE=1`). An `upto` option without `extra.facilitatorAddress` is refused, never approximated with an upfront transfer. Native-asset and Solana requests, and AgentWallet's own paywalls, are paid by an on-chain transfer proved by transaction hash, which is what those servers verify.
|
|
247
247
|
|
|
248
|
-
**Approvals above the cap.** On a hosted wallet, a payment above the cap does not have to fail. With `request_approval` (default on; `AGENTWALLET_APPROVALS=0` turns it off) the wallet owner gets an email with approve and deny links, `pay_x402` returns an `approval_id`, and the agent retries with it once `check_approval` says approved. An approval is bound to one wallet, chain, asset, recipient and maximum, expires in 24 hours, and is consumed by one signature. Local mode has no approval channel: the caps in your environment are the policy.
|
|
248
|
+
**Approvals above the cap.** On a hosted wallet, a payment above the cap does not have to fail. With `request_approval` (default on; `AGENTWALLET_APPROVALS=0` turns it off) the wallet owner gets an email with approve and deny links, `pay_x402` returns an `approval_id`, and the agent retries with it once `check_approval` says approved. An approval is bound to one wallet, chain, asset, recipient and maximum, expires in 24 hours, and is consumed by one signature. Local mode has no approval channel: the caps in your environment are the policy, and an `approval_id` passed there is refused.
|
|
249
249
|
|
|
250
250
|
**Asset risk.** `check_token_risk` (and every `approve_token` result) reports honeypot, tax, owner-power, verified-source, holder-concentration and liquidity flags from GoPlus Security, with an on-chain fallback. A warning for the caller to weigh, never a block.
|
|
251
251
|
|
package/build/index.js
CHANGED
|
@@ -13,7 +13,7 @@ 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
15
|
import { assertPublicUrl, safeFetch, FINAL_URL_HEADER } from './ssrf-guard.js';
|
|
16
|
-
import { deriveX402Payment, isWithinCap, lookupTrustedDecimals, assertDeclaredDecimals, nativeDecimals, SOLANA_CHAIN_IDS, assetLabel, autopayAssetAllowed, autopayEnvCap, effectiveAutopayCap, maxAuthWindowSeconds, parseDecimalPins, } from './x402-payment.js';
|
|
16
|
+
import { deriveX402Payment, isWithinCap, lookupTrustedDecimals, assertDeclaredDecimals, nativeDecimals, SOLANA_CHAIN_IDS, assetLabel, autopayAssetAllowed, autopayEnvCap, effectiveAutopayCap, maxAuthWindowSeconds, parseDecimalPins, approvalRefusal, } from './x402-payment.js';
|
|
17
17
|
import { isLocalMode, getLocalAddress, localWalletRecord, localSend, localNativeBalance, localTokenBalance, localSignMessage, resolveRpcUrl, } from './local-wallet.js';
|
|
18
18
|
import { localSignAuthorization, localEthCall, localGetCode, localSignPermit2Upto } from './local-wallet.js';
|
|
19
19
|
import { WRAPPED_NATIVE } from './wrapped-native.js';
|
|
@@ -561,7 +561,7 @@ function resolveChainId(network) {
|
|
|
561
561
|
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');
|
|
562
562
|
const server = new McpServer({
|
|
563
563
|
name: 'agentwallet',
|
|
564
|
-
version: '1.13.
|
|
564
|
+
version: '1.13.2',
|
|
565
565
|
}, {
|
|
566
566
|
instructions: `AgentWallet gives AI agents their own blockchain wallets. ${anyLocalMode()
|
|
567
567
|
? '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.'
|
|
@@ -1370,13 +1370,14 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
|
|
|
1370
1370
|
};
|
|
1371
1371
|
const wantApproval = request_approval ?? (process.env.AGENTWALLET_APPROVALS !== '0');
|
|
1372
1372
|
if (approval_id) {
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
const
|
|
1378
|
-
|
|
1379
|
-
|
|
1373
|
+
// Local mode refuses before any lookup: only the hosted signer can consume
|
|
1374
|
+
// an approval, so here the same id would lift the cap on every call.
|
|
1375
|
+
const want = { walletId: wallet_id, chainId, asset: tokenAddress, payTo: option.payTo, rawAmount };
|
|
1376
|
+
const local = anyLocalMode();
|
|
1377
|
+
const a = local ? null : (await api(`/approvals/${encodeURIComponent(approval_id)}`, 'GET', undefined, skip));
|
|
1378
|
+
const refusal = approvalRefusal(approval_id, a, want, local);
|
|
1379
|
+
if (refusal) {
|
|
1380
|
+
return jsonResponse({ ...overCap, approval_id, ...refusal });
|
|
1380
1381
|
}
|
|
1381
1382
|
approvalUsed = String(approval_id);
|
|
1382
1383
|
}
|
package/build/local-solana.js
CHANGED
|
@@ -355,10 +355,19 @@ export async function localSplTransfer(mintStr, to, rawAmount, decimals, chainId
|
|
|
355
355
|
};
|
|
356
356
|
}
|
|
357
357
|
const TOKEN_ACCOUNT_SIZE = 165;
|
|
358
|
+
const MINT_SIZE = 82;
|
|
359
|
+
/** Token-2022 pads a mint to the token-account size and tags the next byte: 1 = mint, 2 = token account. */
|
|
360
|
+
const TOKEN_2022_TYPE_MINT = 1;
|
|
358
361
|
/**
|
|
359
362
|
* A recipient must be something that can own a token account: a wallet or a
|
|
360
|
-
* program-derived wallet (Squads and friends), never a program and never
|
|
361
|
-
* token
|
|
363
|
+
* program-derived wallet (Squads and friends), never a program and never an
|
|
364
|
+
* account the token programs own. SPL sent to ATA(ATA), ATA(mint) or a program
|
|
365
|
+
* id is gone for good, or belongs to whoever kept the mint's keypair.
|
|
366
|
+
*
|
|
367
|
+
* Ownership alone decides. A length gate (>= 165 bytes) used to sit here and
|
|
368
|
+
* let an 82-byte mint through as a "wallet" (2026-10-02 report): everything
|
|
369
|
+
* the token programs own is a mint, a token account or a multisig, and none
|
|
370
|
+
* of those is a wallet.
|
|
362
371
|
*/
|
|
363
372
|
async function assertRecipientIsWallet(conn, recipient) {
|
|
364
373
|
const info = await conn.getAccountInfo(recipient);
|
|
@@ -366,8 +375,16 @@ async function assertRecipientIsWallet(conn, recipient) {
|
|
|
366
375
|
return; // never seen on chain: an ordinary fresh wallet
|
|
367
376
|
if (info.executable)
|
|
368
377
|
throw new Error(`Refusing to send to ${recipient.toBase58()}: it is a program, not a wallet.`);
|
|
369
|
-
if (
|
|
370
|
-
|
|
378
|
+
if (info.owner.equals(TOKEN_PROGRAM) || info.owner.equals(TOKEN_2022_PROGRAM)) {
|
|
379
|
+
const isMint = info.data.length === MINT_SIZE
|
|
380
|
+
|| (info.data.length > TOKEN_ACCOUNT_SIZE && info.data[TOKEN_ACCOUNT_SIZE] === TOKEN_2022_TYPE_MINT);
|
|
381
|
+
if (isMint) {
|
|
382
|
+
throw new Error(`Refusing to send to ${recipient.toBase58()}: it is a token mint, not a wallet. Pass the recipient's wallet address; the mint belongs in token_mint.`);
|
|
383
|
+
}
|
|
384
|
+
if (info.data.length >= TOKEN_ACCOUNT_SIZE) {
|
|
385
|
+
throw new Error(`Refusing to send to ${recipient.toBase58()}: it is a token account, not a wallet. Pass the owner's wallet address instead.`);
|
|
386
|
+
}
|
|
387
|
+
throw new Error(`Refusing to send to ${recipient.toBase58()}: it is an account owned by the SPL token program, not a wallet.`);
|
|
371
388
|
}
|
|
372
389
|
}
|
|
373
390
|
export function solanaWalletRecord() {
|
package/build/x402-payment.d.ts
CHANGED
|
@@ -163,3 +163,35 @@ export declare const SOLANA_CHAIN_IDS: ReadonlySet<number>;
|
|
|
163
163
|
*/
|
|
164
164
|
export declare function nativeDecimals(chainId: number): number;
|
|
165
165
|
export declare function isWithinCap(rawAmountRequired: string, trustedDecimals: number, capHuman: string): boolean;
|
|
166
|
+
/** The row GET /approvals/{id} answers with. Numbers arrive as strings from the server. */
|
|
167
|
+
export interface ApprovalRow {
|
|
168
|
+
status?: string;
|
|
169
|
+
used?: number | string;
|
|
170
|
+
wallet_id?: number | string;
|
|
171
|
+
chain_id?: number | string;
|
|
172
|
+
asset?: string;
|
|
173
|
+
pay_to?: string;
|
|
174
|
+
value?: string;
|
|
175
|
+
error?: string;
|
|
176
|
+
}
|
|
177
|
+
export interface ApprovalWant {
|
|
178
|
+
walletId: number;
|
|
179
|
+
chainId: number;
|
|
180
|
+
asset: string;
|
|
181
|
+
payTo: string;
|
|
182
|
+
rawAmount: string;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Why an approval cannot stand in for the cap on this payment, or null when it can.
|
|
186
|
+
*
|
|
187
|
+
* An approval is single-use because the hosted signer marks it used in the same
|
|
188
|
+
* request that signs. A local signer never talks to that server, so in local
|
|
189
|
+
* mode nothing could ever consume one and the same id would lift the cap again
|
|
190
|
+
* on every call (2026-10-02 report). Local mode therefore refuses approvals
|
|
191
|
+
* outright, before any lookup. For a hosted wallet every bound field is checked
|
|
192
|
+
* here, the wallet included, and the server checks them again when it signs.
|
|
193
|
+
*/
|
|
194
|
+
export declare function approvalRefusal(approvalId: string, row: ApprovalRow | null | undefined, want: ApprovalWant, localMode: boolean): {
|
|
195
|
+
error: string;
|
|
196
|
+
approval_status?: string | null;
|
|
197
|
+
} | null;
|
package/build/x402-payment.js
CHANGED
|
@@ -324,3 +324,37 @@ export function isWithinCap(rawAmountRequired, trustedDecimals, capHuman) {
|
|
|
324
324
|
const capRaw = toBaseUnits(capHuman, trustedDecimals);
|
|
325
325
|
return BigInt(rawAmountRequired) <= BigInt(capRaw);
|
|
326
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Why an approval cannot stand in for the cap on this payment, or null when it can.
|
|
329
|
+
*
|
|
330
|
+
* An approval is single-use because the hosted signer marks it used in the same
|
|
331
|
+
* request that signs. A local signer never talks to that server, so in local
|
|
332
|
+
* mode nothing could ever consume one and the same id would lift the cap again
|
|
333
|
+
* on every call (2026-10-02 report). Local mode therefore refuses approvals
|
|
334
|
+
* outright, before any lookup. For a hosted wallet every bound field is checked
|
|
335
|
+
* here, the wallet included, and the server checks them again when it signs.
|
|
336
|
+
*/
|
|
337
|
+
export function approvalRefusal(approvalId, row, want, localMode) {
|
|
338
|
+
if (localMode) {
|
|
339
|
+
return {
|
|
340
|
+
error: `Approval ${approvalId} cannot be used in local mode: approvals are consumed by the hosted signer, and this server signs with your own key, so nothing could mark it used. Local mode has no approval channel: raise AGENTWALLET_MAX_AUTOPAY or AGENTWALLET_MAX_TX_TOKEN yourself.`,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const status = row?.status ?? null;
|
|
344
|
+
// "used" must read as exactly 0. A missing or unparseable field refuses rather than passes.
|
|
345
|
+
if (status !== 'approved' || Number(row?.used) !== 0) {
|
|
346
|
+
const state = status === 'approved' ? 'already used' : `${status ?? 'unknown'}, not approved`;
|
|
347
|
+
return { approval_status: status, error: `Approval ${approvalId} is ${state}.${row?.error ? ' ' + row.error : ''}` };
|
|
348
|
+
}
|
|
349
|
+
const value = String(row?.value ?? '');
|
|
350
|
+
const covers = Number(row?.wallet_id) === want.walletId
|
|
351
|
+
&& Number(row?.chain_id) === want.chainId
|
|
352
|
+
&& String(row?.asset ?? '').toLowerCase() === want.asset.toLowerCase()
|
|
353
|
+
&& String(row?.pay_to ?? '').toLowerCase() === want.payTo.toLowerCase()
|
|
354
|
+
&& /^\d+$/.test(value) && /^\d+$/.test(want.rawAmount)
|
|
355
|
+
&& BigInt(value) >= BigInt(want.rawAmount);
|
|
356
|
+
if (!covers) {
|
|
357
|
+
return { error: 'Approval does not cover this payment (wallet, asset, recipient, chain or amount differ).' };
|
|
358
|
+
}
|
|
359
|
+
return null;
|
|
360
|
+
}
|
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.
|
|
4
|
+
"version": "1.13.2",
|
|
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",
|
|
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",
|
|
20
20
|
"prepublishOnly": "npm run build"
|
|
21
21
|
},
|
|
22
22
|
"keywords": [
|