@true402.dev/mcp-server 0.5.0 → 0.7.0
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/dist/index.js +1 -1
- package/dist/x402-pay.d.ts +16 -0
- package/dist/x402-pay.js +86 -1
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -22,7 +22,7 @@ const SERVER_URL = process.env.SERVER_URL ?? "https://true402.dev/api";
|
|
|
22
22
|
const WALLET_PRIVATE_KEY = process.env.WALLET_PRIVATE_KEY;
|
|
23
23
|
const server = new McpServer({
|
|
24
24
|
name: "true402",
|
|
25
|
-
version: "0.
|
|
25
|
+
version: "0.7.0",
|
|
26
26
|
});
|
|
27
27
|
// Start server with stdio transport
|
|
28
28
|
async function main() {
|
package/dist/x402-pay.d.ts
CHANGED
|
@@ -47,6 +47,22 @@ export interface EVMPaymentRequirement {
|
|
|
47
47
|
version?: string;
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Guard a 402 requirement BEFORE signing: a hostile/buggy server must not make the wallet sign a
|
|
52
|
+
* draining amount or an off-spec asset. Returns a refusal string, or null if the payment is allowed.
|
|
53
|
+
* - amount must be ≤ `maxUsdc` (the client-side spend ceiling, MAX_PAYMENT_USDC).
|
|
54
|
+
* - asset must be the canonical USDC on a supported chain (no arbitrary token/verifyingContract).
|
|
55
|
+
*/
|
|
56
|
+
export declare function assessRequirement(req: EVMPaymentRequirement, maxUsdc: number): string | null;
|
|
57
|
+
/**
|
|
58
|
+
* Parse the MAX_PAYMENT_USDC ceiling FAIL-CLOSED: unset → the 0.10 default; a non-numeric or negative
|
|
59
|
+
* value (e.g. the European comma-decimal "0,10" → NaN) clamps to 0 = refuse ALL auto-pay, never
|
|
60
|
+
* disables the only wallet-drain guard.
|
|
61
|
+
*/
|
|
62
|
+
export declare function parseMaxUsdc(raw: string | undefined): number;
|
|
63
|
+
/** Refuse to send a signed payment over cleartext (the X-PAYMENT header would be interceptable).
|
|
64
|
+
* Returns a refusal string, or null if the URL is https (or localhost for dev). */
|
|
65
|
+
export declare function requireSecureUrl(url: string): string | null;
|
|
50
66
|
/**
|
|
51
67
|
* Sign an EIP-3009 TransferWithAuthorization and return the x402 payment
|
|
52
68
|
* payload. The EIP-712 domain is derived from the requirement (network +
|
package/dist/x402-pay.js
CHANGED
|
@@ -65,6 +65,73 @@ function resolveChainId(network) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
+
// Canonical USDC per chain — the ONLY token/chain this client will ever sign a payment for, so a
|
|
69
|
+
// hostile or buggy server cannot redirect the wallet's signature to an arbitrary token/chain.
|
|
70
|
+
const USDC_BY_CHAIN = {
|
|
71
|
+
8453: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // Base mainnet
|
|
72
|
+
84532: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // Base Sepolia
|
|
73
|
+
1: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // Ethereum mainnet
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Guard a 402 requirement BEFORE signing: a hostile/buggy server must not make the wallet sign a
|
|
77
|
+
* draining amount or an off-spec asset. Returns a refusal string, or null if the payment is allowed.
|
|
78
|
+
* - amount must be ≤ `maxUsdc` (the client-side spend ceiling, MAX_PAYMENT_USDC).
|
|
79
|
+
* - asset must be the canonical USDC on a supported chain (no arbitrary token/verifyingContract).
|
|
80
|
+
*/
|
|
81
|
+
export function assessRequirement(req, maxUsdc) {
|
|
82
|
+
const raw = req.amount ?? req.maxAmountRequired;
|
|
83
|
+
if (raw === undefined)
|
|
84
|
+
return "the 402 has no amount; refusing to sign.";
|
|
85
|
+
let amountUsdc;
|
|
86
|
+
try {
|
|
87
|
+
amountUsdc = Number(BigInt(raw)) / 1e6; // USDC has 6 decimals
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return "the 402 amount is unparseable; refusing to sign.";
|
|
91
|
+
}
|
|
92
|
+
if (!Number.isFinite(amountUsdc) || amountUsdc < 0)
|
|
93
|
+
return "the 402 amount is invalid; refusing to sign.";
|
|
94
|
+
// `maxUsdc` is a finite, non-negative invariant (see parseMaxUsdc) — the comparison ALWAYS runs, so
|
|
95
|
+
// a misconfigured ceiling can never silently disable the cap (maxUsdc=0 refuses everything).
|
|
96
|
+
if (amountUsdc > maxUsdc) {
|
|
97
|
+
return `refusing to auto-pay $${amountUsdc} USDC — it exceeds the MAX_PAYMENT_USDC ceiling of $${maxUsdc}. Raise MAX_PAYMENT_USDC if this is intended.`;
|
|
98
|
+
}
|
|
99
|
+
const usdc = USDC_BY_CHAIN[resolveChainId(req.network)];
|
|
100
|
+
if (!usdc || (req.asset ?? "").toLowerCase() !== usdc.toLowerCase()) {
|
|
101
|
+
return "refusing to pay: the 402's asset/network is not canonical USDC on a supported Base network.";
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Parse the MAX_PAYMENT_USDC ceiling FAIL-CLOSED: unset → the 0.10 default; a non-numeric or negative
|
|
107
|
+
* value (e.g. the European comma-decimal "0,10" → NaN) clamps to 0 = refuse ALL auto-pay, never
|
|
108
|
+
* disables the only wallet-drain guard.
|
|
109
|
+
*/
|
|
110
|
+
export function parseMaxUsdc(raw) {
|
|
111
|
+
if (raw === undefined)
|
|
112
|
+
return 0.1;
|
|
113
|
+
const n = Number(raw);
|
|
114
|
+
if (Number.isFinite(n) && n >= 0)
|
|
115
|
+
return n;
|
|
116
|
+
console.error(`true402 MCP: MAX_PAYMENT_USDC='${raw}' is not a valid number — refusing ALL auto-pay. Set a numeric value like 0.10.`);
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
/** Refuse to send a signed payment over cleartext (the X-PAYMENT header would be interceptable).
|
|
120
|
+
* Returns a refusal string, or null if the URL is https (or localhost for dev). */
|
|
121
|
+
export function requireSecureUrl(url) {
|
|
122
|
+
let u;
|
|
123
|
+
try {
|
|
124
|
+
u = new URL(url);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return `invalid SERVER_URL: ${url}`;
|
|
128
|
+
}
|
|
129
|
+
const local = u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "::1";
|
|
130
|
+
if (u.protocol !== "https:" && !local) {
|
|
131
|
+
return `refusing to send a signed payment over cleartext (${u.protocol}//${u.hostname}); use an https SERVER_URL (or localhost for dev).`;
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
68
135
|
/**
|
|
69
136
|
* Sign an EIP-3009 TransferWithAuthorization and return the x402 payment
|
|
70
137
|
* payload. The EIP-712 domain is derived from the requirement (network +
|
|
@@ -78,7 +145,14 @@ export async function signEIP3009Payment(privateKey, requirement) {
|
|
|
78
145
|
const account = privateKeyToAccount(key);
|
|
79
146
|
const now = Math.floor(Date.now() / 1000);
|
|
80
147
|
const validAfter = BigInt(now - 60); // valid 60s in the past (clock skew tolerance)
|
|
81
|
-
|
|
148
|
+
// Validate + bound the signed window: maxTimeoutSeconds arrives via JSON and may be a string
|
|
149
|
+
// (`now + "999"` would string-concat); a hostile 402 must not keep the authorization broadcastable
|
|
150
|
+
// far into the future. Coerce, reject garbage, cap at 10 minutes.
|
|
151
|
+
const timeout = Number(requirement.maxTimeoutSeconds);
|
|
152
|
+
if (!Number.isInteger(timeout) || timeout <= 0) {
|
|
153
|
+
throw new Error("Payment requirement has an invalid maxTimeoutSeconds");
|
|
154
|
+
}
|
|
155
|
+
const validBefore = BigInt(now + Math.min(timeout, 600));
|
|
82
156
|
// Generate a random nonce (bytes32)
|
|
83
157
|
const randomBytes = new Uint8Array(32);
|
|
84
158
|
crypto.getRandomValues(randomBytes);
|
|
@@ -150,6 +224,10 @@ export async function signEIP3009Payment(privateKey, requirement) {
|
|
|
150
224
|
*/
|
|
151
225
|
export async function payAndFetch(baseUrl, path, body, walletPrivateKey) {
|
|
152
226
|
const url = `${baseUrl}${path}`;
|
|
227
|
+
// Step 0: never sign/send a payment over cleartext (X-PAYMENT would be interceptable).
|
|
228
|
+
const insecure = requireSecureUrl(url);
|
|
229
|
+
if (insecure)
|
|
230
|
+
return { ok: false, message: insecure };
|
|
153
231
|
// Step 1: first request, no payment header.
|
|
154
232
|
let firstResponse;
|
|
155
233
|
try {
|
|
@@ -210,6 +288,13 @@ export async function payAndFetch(baseUrl, path, body, walletPrivateKey) {
|
|
|
210
288
|
].join("\n"),
|
|
211
289
|
};
|
|
212
290
|
}
|
|
291
|
+
// Spend ceiling + asset/network pin: a hostile server must not make our wallet sign a draining
|
|
292
|
+
// amount or a payment to an arbitrary token/chain. Refuse BEFORE signing.
|
|
293
|
+
const maxUsdc = parseMaxUsdc(process.env.MAX_PAYMENT_USDC);
|
|
294
|
+
const refusal = assessRequirement(evmRequirement, maxUsdc);
|
|
295
|
+
if (refusal) {
|
|
296
|
+
return { ok: false, paymentRequired: true, message: refusal };
|
|
297
|
+
}
|
|
213
298
|
// Step 4: sign the EIP-3009 authorization and base64-encode the payload.
|
|
214
299
|
let xPaymentHeader;
|
|
215
300
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@true402.dev/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"mcpName": "dev.true402/mcp-server",
|
|
5
5
|
"description": "MCP server for the true402 machine-native marketplace — pay-per-call AI + web + Base on-chain tools over x402 (USDC on Base): LLM inference, SEO/GEO audit, web extract, link preview, robots/AI-crawler check, security headers, and on-chain DeFi trading signals (token rug/honeypot safety, new token pairs, liquidity-pull/rug alerts, whale swaps).",
|
|
6
6
|
"type": "module",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"build": "tsc",
|
|
17
17
|
"start": "node dist/index.js",
|
|
18
18
|
"dev": "tsx src/index.ts",
|
|
19
|
+
"test": "vitest run",
|
|
19
20
|
"prepublishOnly": "npm run build"
|
|
20
21
|
},
|
|
21
22
|
"keywords": [
|
|
@@ -51,7 +52,8 @@
|
|
|
51
52
|
"devDependencies": {
|
|
52
53
|
"@types/node": "^22.0.0",
|
|
53
54
|
"tsx": "^4.19.0",
|
|
54
|
-
"typescript": "^5.7.0"
|
|
55
|
+
"typescript": "^5.7.0",
|
|
56
|
+
"vitest": "^2.1.0"
|
|
55
57
|
},
|
|
56
58
|
"engines": {
|
|
57
59
|
"node": ">=20.0.0"
|