z-zero-mcp-server 1.0.7 → 1.1.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/api_backend.js +44 -11
- package/dist/index.js +485 -37
- package/dist/lib/extract-total-price.d.ts +9 -0
- package/dist/lib/extract-total-price.js +127 -0
- package/dist/lib/key-store.d.ts +13 -0
- package/dist/lib/key-store.js +38 -0
- package/dist/lib/web3-detector.d.ts +20 -0
- package/dist/lib/web3-detector.js +182 -0
- package/dist/lib/with-timeout.d.ts +21 -0
- package/dist/lib/with-timeout.js +45 -0
- package/dist/playwright_bridge.d.ts +1 -0
- package/dist/playwright_bridge.js +34 -2
- package/dist/types.d.ts +1 -0
- package/dist/wdk_backend.d.ts +9 -0
- package/dist/wdk_backend.js +228 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,26 +1,85 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
-
// OpenClaw MCP Server (z-zero-mcp-server) v1.0
|
|
3
|
+
// OpenClaw MCP Server (z-zero-mcp-server) v1.1.0
|
|
4
4
|
// Exposes secure JIT payment tools to AI Agents via Model Context Protocol
|
|
5
5
|
// Status: Connected to Z-ZERO Gateway — produces secure JIT virtual cards
|
|
6
|
+
// WDK Mode: Set Z_ZERO_WALLET_MODE=wdk for non-custodial WDK payments
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
6
40
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
const CURRENT_MCP_VERSION = "1.1.0"; // ← bump this on every release
|
|
42
|
+
const ZZERO_VERSION_API = "https://www.clawcard.store/api/version";
|
|
7
43
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
8
44
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
9
45
|
const zod_1 = require("zod");
|
|
10
|
-
|
|
46
|
+
// ── Backend selection (resolved at startup, before server init) ──────────────
|
|
47
|
+
// tsx / Node ESM: import() is async, so we use a synchronous pattern here.
|
|
48
|
+
// Both backends export identical function signatures (drop-in swap).
|
|
49
|
+
const WALLET_MODE = process.env.Z_ZERO_WALLET_MODE || "custodial";
|
|
50
|
+
const isWDKMode = WALLET_MODE === "wdk";
|
|
51
|
+
// Import both backends; pick which one to use based on env var.
|
|
52
|
+
// In production build, tree-shaking will handle this. In dev (tsx), both load.
|
|
53
|
+
const custodialBackend = __importStar(require("./api_backend.js"));
|
|
54
|
+
const wdkBackend = __importStar(require("./wdk_backend.js"));
|
|
55
|
+
const activeBackend = isWDKMode ? wdkBackend : custodialBackend;
|
|
56
|
+
const issueTokenRemote = activeBackend.issueTokenRemote;
|
|
57
|
+
const resolveTokenRemote = activeBackend.resolveTokenRemote;
|
|
58
|
+
const burnTokenRemote = activeBackend.burnTokenRemote;
|
|
59
|
+
const cancelTokenRemote = activeBackend.cancelTokenRemote;
|
|
60
|
+
const refundUnderspendRemote = activeBackend.refundUnderspendRemote;
|
|
61
|
+
const getBalanceRemote = activeBackend.getBalanceRemote;
|
|
62
|
+
const listCardsRemote = activeBackend.listCardsRemote;
|
|
63
|
+
const getDepositAddressesRemote = activeBackend.getDepositAddressesRemote;
|
|
64
|
+
console.error(`[Z-ZERO MCP] 🚀 Wallet mode: ${WALLET_MODE.toUpperCase()}`);
|
|
65
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
11
66
|
const playwright_bridge_js_1 = require("./playwright_bridge.js");
|
|
67
|
+
const web3_detector_js_1 = require("./lib/web3-detector.js");
|
|
68
|
+
const extract_total_price_js_1 = require("./lib/extract-total-price.js");
|
|
69
|
+
const playwright_1 = require("playwright");
|
|
70
|
+
const key_store_js_1 = require("./lib/key-store.js"); // ✅ Hot-Swap support
|
|
12
71
|
// ============================================================
|
|
13
72
|
// CREATE MCP SERVER
|
|
14
73
|
// ============================================================
|
|
15
74
|
const server = new mcp_js_1.McpServer({
|
|
16
75
|
name: "z-zero-mcp-server",
|
|
17
|
-
version: "
|
|
76
|
+
version: "2.0.0",
|
|
18
77
|
});
|
|
19
78
|
// ============================================================
|
|
20
79
|
// TOOL 1: List available cards (safe - no sensitive data)
|
|
21
80
|
// ============================================================
|
|
22
81
|
server.tool("list_cards", "List all available virtual card aliases and their balances. No sensitive data is returned.", {}, async () => {
|
|
23
|
-
const data = await
|
|
82
|
+
const data = await listCardsRemote();
|
|
24
83
|
if (data?.error === "AUTH_REQUIRED") {
|
|
25
84
|
return {
|
|
26
85
|
content: [{
|
|
@@ -43,7 +102,7 @@ server.tool("list_cards", "List all available virtual card aliases and their bal
|
|
|
43
102
|
}
|
|
44
103
|
const cards = data?.cards || [];
|
|
45
104
|
const activeTokens = data?.active_tokens || [];
|
|
46
|
-
const
|
|
105
|
+
const historySummary = data?.history_summary || {};
|
|
47
106
|
return {
|
|
48
107
|
content: [
|
|
49
108
|
{
|
|
@@ -51,8 +110,8 @@ server.tool("list_cards", "List all available virtual card aliases and their bal
|
|
|
51
110
|
text: JSON.stringify({
|
|
52
111
|
cards,
|
|
53
112
|
active_tokens: activeTokens,
|
|
54
|
-
|
|
55
|
-
note: "Use card aliases to request payment tokens.
|
|
113
|
+
history_summary: historySummary,
|
|
114
|
+
note: "Only active tokens are shown. Use card aliases to request payment tokens.",
|
|
56
115
|
}, null, 2),
|
|
57
116
|
},
|
|
58
117
|
],
|
|
@@ -66,7 +125,7 @@ server.tool("check_balance", "Check the wallet balance (human-level total). This
|
|
|
66
125
|
.string()
|
|
67
126
|
.describe("The alias of the card to check, e.g. 'Card_01'"),
|
|
68
127
|
}, async ({ card_alias }) => {
|
|
69
|
-
const data = await
|
|
128
|
+
const data = await getBalanceRemote(card_alias);
|
|
70
129
|
if (data?.error === "AUTH_REQUIRED") {
|
|
71
130
|
return {
|
|
72
131
|
content: [{
|
|
@@ -102,7 +161,7 @@ server.tool("check_balance", "Check the wallet balance (human-level total). This
|
|
|
102
161
|
// TOOL 2.5: Get deposit addresses (Phase 14 feature)
|
|
103
162
|
// ============================================================
|
|
104
163
|
server.tool("get_deposit_addresses", "Get your unique deposit addresses for EVM networks (Base, BSC, Ethereum) and Tron. Provide these to the human user when they need to add funds to their Z-ZERO balance.", {}, async () => {
|
|
105
|
-
const data = await
|
|
164
|
+
const data = await getDepositAddressesRemote();
|
|
106
165
|
if (data?.error === "AUTH_REQUIRED") {
|
|
107
166
|
return {
|
|
108
167
|
content: [{
|
|
@@ -114,39 +173,55 @@ server.tool("get_deposit_addresses", "Get your unique deposit addresses for EVM
|
|
|
114
173
|
isError: true
|
|
115
174
|
};
|
|
116
175
|
}
|
|
176
|
+
// ── WDK Non-Custodial Mode ────────────────────────────────────────────
|
|
177
|
+
if (data?.wdk_wallet?.address) {
|
|
178
|
+
const wdkAddr = data.wdk_wallet.address;
|
|
179
|
+
const balance = data.wdk_wallet.balance_usdt ?? 0;
|
|
180
|
+
return {
|
|
181
|
+
content: [{
|
|
182
|
+
type: "text",
|
|
183
|
+
text: JSON.stringify({
|
|
184
|
+
wallet_type: "non-custodial (WDK)",
|
|
185
|
+
address: wdkAddr,
|
|
186
|
+
balance_usdt: balance,
|
|
187
|
+
supported_chains: [
|
|
188
|
+
{ chain: "Polygon", token: "USDT", address: wdkAddr },
|
|
189
|
+
],
|
|
190
|
+
instructions: `Send USDT (Polygon/ERC-20) to this address: ${wdkAddr}. Funds will appear in your agent wallet within 1-3 minutes.`,
|
|
191
|
+
note: "Gasless payments via ERC-4337 Paymaster. Your agent pays ~$0.001 in gas per tx."
|
|
192
|
+
}, null, 2),
|
|
193
|
+
}],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
// ── Custodial Mode Fallback ───────────────────────────────────────────
|
|
117
197
|
const addresses = data?.deposit_addresses;
|
|
118
198
|
if (!addresses) {
|
|
119
199
|
return {
|
|
120
|
-
content: [
|
|
121
|
-
{
|
|
200
|
+
content: [{
|
|
122
201
|
type: "text",
|
|
123
202
|
text: "Failed to retrieve deposit addresses. Please ensure your Z_ZERO_API_KEY (Passport Key) is valid. You can find it at https://www.clawcard.store/dashboard/agents",
|
|
124
|
-
},
|
|
125
|
-
],
|
|
203
|
+
}],
|
|
126
204
|
isError: true,
|
|
127
205
|
};
|
|
128
206
|
}
|
|
129
207
|
return {
|
|
130
|
-
content: [
|
|
131
|
-
{
|
|
208
|
+
content: [{
|
|
132
209
|
type: "text",
|
|
133
210
|
text: JSON.stringify({
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
211
|
+
wallet_type: "custodial",
|
|
212
|
+
evm: {
|
|
213
|
+
address: addresses.evm,
|
|
214
|
+
supported_chains: ["Base", "BNB Smart Chain (BSC)", "Ethereum"],
|
|
215
|
+
tokens: ["USDC", "USDT"]
|
|
216
|
+
},
|
|
217
|
+
tron: {
|
|
218
|
+
address: addresses.tron,
|
|
219
|
+
supported_chains: ["Tron (TRC-20)"],
|
|
220
|
+
tokens: ["USDT"]
|
|
145
221
|
},
|
|
146
222
|
note: "Funds sent to these addresses will be automatically credited to your Z-ZERO balance within minutes."
|
|
147
223
|
}, null, 2),
|
|
148
|
-
},
|
|
149
|
-
],
|
|
224
|
+
}],
|
|
150
225
|
};
|
|
151
226
|
});
|
|
152
227
|
// ============================================================
|
|
@@ -165,7 +240,7 @@ server.tool("request_payment_token", "Request a temporary payment token for a sp
|
|
|
165
240
|
.string()
|
|
166
241
|
.describe("Name or URL of the merchant/service being purchased"),
|
|
167
242
|
}, async ({ card_alias, amount, merchant }) => {
|
|
168
|
-
const token = await
|
|
243
|
+
const token = await issueTokenRemote(card_alias, amount, merchant);
|
|
169
244
|
if (token?.error === "AUTH_REQUIRED") {
|
|
170
245
|
return {
|
|
171
246
|
content: [{
|
|
@@ -187,7 +262,7 @@ server.tool("request_payment_token", "Request a temporary payment token for a sp
|
|
|
187
262
|
isError: true,
|
|
188
263
|
};
|
|
189
264
|
}
|
|
190
|
-
const balanceData = await
|
|
265
|
+
const balanceData = await getBalanceRemote(card_alias);
|
|
191
266
|
const balance = balanceData?.balance;
|
|
192
267
|
return {
|
|
193
268
|
content: [
|
|
@@ -208,6 +283,7 @@ server.tool("request_payment_token", "Request a temporary payment token for a sp
|
|
|
208
283
|
type: "text",
|
|
209
284
|
text: JSON.stringify({
|
|
210
285
|
token: token.token,
|
|
286
|
+
token_ref: `...${token.token.slice(-6)}`,
|
|
211
287
|
amount: token.amount,
|
|
212
288
|
merchant: token.merchant,
|
|
213
289
|
expires_at: expiresAt,
|
|
@@ -235,7 +311,7 @@ server.tool("execute_payment", "Execute a payment using a temporary token. This
|
|
|
235
311
|
.describe("The actual final amount on the checkout page. If different from token amount, system will auto-refund the difference."),
|
|
236
312
|
}, async ({ token, checkout_url, actual_amount }) => {
|
|
237
313
|
// Step 1: Resolve token → card data (RAM only)
|
|
238
|
-
const cardData = await
|
|
314
|
+
const cardData = await resolveTokenRemote(token);
|
|
239
315
|
if (cardData?.error === "AUTH_REQUIRED") {
|
|
240
316
|
return {
|
|
241
317
|
content: [{
|
|
@@ -257,13 +333,60 @@ server.tool("execute_payment", "Execute a payment using a temporary token. This
|
|
|
257
333
|
isError: true,
|
|
258
334
|
};
|
|
259
335
|
}
|
|
336
|
+
// 🔒 PRE-FLIGHT AMOUNT GUARD (Prompt Injection Defense)
|
|
337
|
+
// If the agent passes actual_amount, verify it doesn't exceed the token's authorized amount.
|
|
338
|
+
// Tolerance: 5% to allow for minor price rounding (e.g., taxes calculated at checkout).
|
|
339
|
+
// Attack scenario: Merchant shows $99 on page, but agent was authorized $10 → block + alert human.
|
|
340
|
+
if (actual_amount !== undefined && cardData.authorized_amount !== undefined) {
|
|
341
|
+
const tokenAmount = Number(cardData.authorized_amount);
|
|
342
|
+
const TOLERANCE = 1.05; // 5% buffer
|
|
343
|
+
if (actual_amount > tokenAmount * TOLERANCE) {
|
|
344
|
+
// Auto-cancel the token to free up funds
|
|
345
|
+
await cancelTokenRemote(token);
|
|
346
|
+
return {
|
|
347
|
+
content: [{
|
|
348
|
+
type: "text",
|
|
349
|
+
text: JSON.stringify({
|
|
350
|
+
success: false,
|
|
351
|
+
blocked: true,
|
|
352
|
+
reason: "PRICE_MISMATCH",
|
|
353
|
+
message: `🚨 PAYMENT BLOCKED: Checkout shows $${actual_amount} but token only authorizes $${tokenAmount}. Token has been cancelled and funds returned to wallet.`,
|
|
354
|
+
token_status: "CANCELLED",
|
|
355
|
+
action_required: "Request a new token with the correct amount if you wish to proceed.",
|
|
356
|
+
}, null, 2),
|
|
357
|
+
}],
|
|
358
|
+
isError: true,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
}
|
|
260
362
|
// Step 2: Use Playwright to inject card into checkout form
|
|
261
363
|
const result = await (0, playwright_bridge_js_1.fillCheckoutForm)(checkout_url, cardData);
|
|
262
|
-
// Step 3: Burn the token
|
|
263
|
-
|
|
364
|
+
// Step 3: Burn the token ONLY if payment succeeded
|
|
365
|
+
// If merchant declines → keep token ACTIVE so webhook decline flow can refund correctly
|
|
366
|
+
if (result.success) {
|
|
367
|
+
await burnTokenRemote(token);
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
// Payment failed — do NOT burn token
|
|
371
|
+
// Airwallex will fire a 'card.authorization.declined' webhook → refund handled there
|
|
372
|
+
return {
|
|
373
|
+
content: [
|
|
374
|
+
{
|
|
375
|
+
type: "text",
|
|
376
|
+
text: JSON.stringify({
|
|
377
|
+
success: false,
|
|
378
|
+
message: result.message || "Payment was declined by merchant.",
|
|
379
|
+
token_status: "ACTIVE",
|
|
380
|
+
note: "Token NOT burned. Funds will be refunded automatically via webhook within minutes.",
|
|
381
|
+
}, null, 2),
|
|
382
|
+
},
|
|
383
|
+
],
|
|
384
|
+
isError: true,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
264
387
|
// Step 4: Refund underspend if actual amount was less than token amount
|
|
265
388
|
if (actual_amount !== undefined && result.success) {
|
|
266
|
-
await
|
|
389
|
+
await refundUnderspendRemote(token, actual_amount);
|
|
267
390
|
}
|
|
268
391
|
// Step 5: Return result (NEVER includes card numbers)
|
|
269
392
|
return {
|
|
@@ -292,7 +415,7 @@ server.tool("cancel_payment_token", "Cancel a payment token that has not been us
|
|
|
292
415
|
.string()
|
|
293
416
|
.describe("Reason for cancellation, e.g. 'Price mismatch: checkout shows $20 but token is $15'"),
|
|
294
417
|
}, async ({ token, reason }) => {
|
|
295
|
-
const result = await
|
|
418
|
+
const result = await cancelTokenRemote(token);
|
|
296
419
|
if (result?.error === "AUTH_REQUIRED") {
|
|
297
420
|
return {
|
|
298
421
|
content: [{
|
|
@@ -372,6 +495,331 @@ server.tool("request_human_approval", "Request human approval before proceeding
|
|
|
372
495
|
};
|
|
373
496
|
});
|
|
374
497
|
// ============================================================
|
|
498
|
+
// TOOL 6.4: Check for Updates (polls clawcard.store, not npm)
|
|
499
|
+
// ============================================================
|
|
500
|
+
server.tool("check_for_updates", "Check if a newer version of this MCP server is available. Polls clawcard.store/api/version — independent of npm so works even if distribution changes. Call this when: (1) user asks if MCP is up to date, (2) a payment fails and you want to rule out a stale MCP version.", {}, async () => {
|
|
501
|
+
try {
|
|
502
|
+
const res = await fetch(ZZERO_VERSION_API, {
|
|
503
|
+
headers: { "User-Agent": `z-zero-mcp/${CURRENT_MCP_VERSION}` },
|
|
504
|
+
signal: AbortSignal.timeout(8_000),
|
|
505
|
+
});
|
|
506
|
+
if (!res.ok)
|
|
507
|
+
throw new Error(`HTTP ${res.status}`);
|
|
508
|
+
const data = await res.json();
|
|
509
|
+
const latest = data.latest_version;
|
|
510
|
+
const isUpToDate = CURRENT_MCP_VERSION === latest;
|
|
511
|
+
return {
|
|
512
|
+
content: [{
|
|
513
|
+
type: "text",
|
|
514
|
+
text: JSON.stringify({
|
|
515
|
+
current_version: CURRENT_MCP_VERSION,
|
|
516
|
+
latest_version: latest,
|
|
517
|
+
up_to_date: isUpToDate,
|
|
518
|
+
status: isUpToDate ? "✅ You are on the latest version." : `⚠️ Update available: ${CURRENT_MCP_VERSION} → ${latest}`,
|
|
519
|
+
release_notes: isUpToDate ? null : data.release_notes,
|
|
520
|
+
how_to_update: isUpToDate ? null : data.update_instructions,
|
|
521
|
+
}, null, 2),
|
|
522
|
+
}],
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
catch (err) {
|
|
526
|
+
return {
|
|
527
|
+
content: [{
|
|
528
|
+
type: "text",
|
|
529
|
+
text: JSON.stringify({
|
|
530
|
+
current_version: CURRENT_MCP_VERSION,
|
|
531
|
+
status: "⚠️ Could not reach version server. Check your internet connection.",
|
|
532
|
+
error: err.message,
|
|
533
|
+
fallback_check: `Visit https://www.clawcard.store to see latest version.`,
|
|
534
|
+
}, null, 2),
|
|
535
|
+
}],
|
|
536
|
+
isError: true,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
// ============================================================
|
|
541
|
+
// TOOL 6.5: Set API Key (Hot-Swap Passport Key — NO restart needed)
|
|
542
|
+
// ============================================================
|
|
543
|
+
server.tool("set_api_key", "Update the Z-ZERO Passport Key immediately WITHOUT restarting the AI tool. Call this when the human provides a new key (e.g. 'zk_live_xxxxx'). The key is validated and activated instantly for all subsequent API calls. IMPORTANT: Never ask for the key proactively — only call this when the human explicitly provides it.", {
|
|
544
|
+
api_key: zod_1.z
|
|
545
|
+
.string()
|
|
546
|
+
.describe("The new Passport Key to activate. Must start with 'zk_live_' or 'zk_test_'. Get from: https://www.clawcard.store/dashboard/agents"),
|
|
547
|
+
}, async ({ api_key }) => {
|
|
548
|
+
const result = (0, key_store_js_1.setPassportKey)(api_key);
|
|
549
|
+
if (!result.ok) {
|
|
550
|
+
return {
|
|
551
|
+
content: [{ type: "text", text: `❌ ${result.message}` }],
|
|
552
|
+
isError: true,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
return {
|
|
556
|
+
content: [{
|
|
557
|
+
type: "text",
|
|
558
|
+
text: JSON.stringify({
|
|
559
|
+
status: "SUCCESS",
|
|
560
|
+
message: `✅ ${result.message}`,
|
|
561
|
+
active_key_prefix: api_key.slice(0, 12) + "...",
|
|
562
|
+
note: "All subsequent API calls will use this key. No restart needed.",
|
|
563
|
+
}, null, 2),
|
|
564
|
+
}],
|
|
565
|
+
};
|
|
566
|
+
});
|
|
567
|
+
// ============================================================
|
|
568
|
+
// TOOL 6.6: Show current API Key status (for debugging)
|
|
569
|
+
// ============================================================
|
|
570
|
+
server.tool("show_api_key_status", "Show whether a Passport Key is currently configured, and its prefix (first 12 chars). Does NOT reveal the full key. Use this to debug authentication issues.", {}, async () => {
|
|
571
|
+
const key = (0, key_store_js_1.getPassportKey)();
|
|
572
|
+
const hasKey = key.length > 0;
|
|
573
|
+
return {
|
|
574
|
+
content: [{
|
|
575
|
+
type: "text",
|
|
576
|
+
text: JSON.stringify({
|
|
577
|
+
configured: hasKey,
|
|
578
|
+
key_prefix: hasKey ? key.slice(0, 12) + "..." : null,
|
|
579
|
+
wallet_mode: process.env.Z_ZERO_WALLET_MODE || "custodial",
|
|
580
|
+
note: hasKey
|
|
581
|
+
? "Key is active. Call set_api_key to update it."
|
|
582
|
+
: "No key configured. Call set_api_key with your Passport Key from https://www.clawcard.store/dashboard/agents",
|
|
583
|
+
}, null, 2),
|
|
584
|
+
}],
|
|
585
|
+
};
|
|
586
|
+
});
|
|
587
|
+
// ============================================================
|
|
588
|
+
// TOOL 7: Auto Pay Checkout (Phase 2 — Smart Routing)
|
|
589
|
+
// ============================================================
|
|
590
|
+
server.tool("auto_pay_checkout", "[Phase 2] Autonomous Smart Routing checkout tool. Provide a checkout URL and this tool will:\n" +
|
|
591
|
+
"1. Scan the page to detect if it supports Web3 (Crypto) payments via window.ethereum or EIP-681 links.\n" +
|
|
592
|
+
"2. SCENARIO A (Web3): If detected, automatically send USDT on-chain via WDK (gas ~$0.001). No Visa card needed.\n" +
|
|
593
|
+
"3. SCENARIO B (Fiat): If no Web3 detected, scan DOM for total price, issue a JIT Visa card for exact amount, auto-fill form.", {
|
|
594
|
+
checkout_url: zod_1.z
|
|
595
|
+
.string()
|
|
596
|
+
.url()
|
|
597
|
+
.describe("Full URL of the checkout/payment page to analyze and pay."),
|
|
598
|
+
card_alias: zod_1.z
|
|
599
|
+
.string()
|
|
600
|
+
.describe("Card alias to charge for JIT Fiat fallback, e.g. 'Card_01'. Required even if Web3 route is used (used for WDK wallet lookup)."),
|
|
601
|
+
}, async ({ checkout_url, card_alias }) => {
|
|
602
|
+
const ZZERO_API = process.env.Z_ZERO_API_BASE || "https://www.clawcard.store";
|
|
603
|
+
const API_KEY = process.env.Z_ZERO_API_KEY || "";
|
|
604
|
+
// ── C2 FIX: API key guard ────────────────────────────────────────────
|
|
605
|
+
if (!API_KEY) {
|
|
606
|
+
return {
|
|
607
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
608
|
+
status: "CONFIG_ERROR",
|
|
609
|
+
message: "Z_ZERO_API_KEY is not configured on this MCP server. Cannot proceed.",
|
|
610
|
+
}, null, 2) }],
|
|
611
|
+
isError: true,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
// ── C1 FIX: SSRF guard — validate checkout_url ───────────────────────
|
|
615
|
+
// Block non-https and internal/private IP ranges
|
|
616
|
+
(() => {
|
|
617
|
+
let url;
|
|
618
|
+
try {
|
|
619
|
+
url = new URL(checkout_url);
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
throw new Error(`Invalid checkout_url: ${checkout_url}`);
|
|
623
|
+
}
|
|
624
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
625
|
+
const hostname = url.hostname;
|
|
626
|
+
// ✅ BUG 4 FIX: Allow http://localhost in dev for testing local checkout pages
|
|
627
|
+
if (isDev && (hostname === "localhost" || hostname === "127.0.0.1")) {
|
|
628
|
+
console.error(`[SMART ROUTE] Dev mode: allowing localhost checkout URL`);
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
if (url.protocol !== "https:") {
|
|
632
|
+
throw new Error(`checkout_url must use HTTPS. Got: ${url.protocol}`);
|
|
633
|
+
}
|
|
634
|
+
const privatePatterns = [
|
|
635
|
+
/^localhost$/i, /^127\./, /^10\./,
|
|
636
|
+
/^172\.(1[6-9]|2\d|3[01])\./, /^192\.168\./,
|
|
637
|
+
/^169\.254\./, /^\[::1\]$/, /^0\.0\.0\.0$/,
|
|
638
|
+
];
|
|
639
|
+
if (privatePatterns.some(p => p.test(hostname))) {
|
|
640
|
+
throw new Error(`SSRF blocked: checkout_url points to private/internal host: ${hostname}`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
})();
|
|
644
|
+
// ── STEP A: Try to detect Web3 payment on the page ──────────────────
|
|
645
|
+
console.error(`[SMART ROUTE] Scanning ${checkout_url} for Web3 payment...`);
|
|
646
|
+
const web3Result = await (0, web3_detector_js_1.detectWeb3Payment)(checkout_url);
|
|
647
|
+
if (web3Result.detected && web3Result.params) {
|
|
648
|
+
// ── SCENARIO A: Web3 Payment Detected ────────────────────────────
|
|
649
|
+
const { to, eip681_amount, data } = web3Result.params;
|
|
650
|
+
// If amount is baked into EIP-681 link, use it. Otherwise need user to confirm.
|
|
651
|
+
if (!eip681_amount && !data) {
|
|
652
|
+
return {
|
|
653
|
+
content: [{
|
|
654
|
+
type: "text",
|
|
655
|
+
text: JSON.stringify({
|
|
656
|
+
route: "WEB3",
|
|
657
|
+
detected: true,
|
|
658
|
+
status: "AMOUNT_REQUIRED",
|
|
659
|
+
recipient: to,
|
|
660
|
+
message: `Web3 payment gateway detected (${web3Result.method}), recipient: ${to}. Could not determine exact USDT amount automatically. Please confirm the amount with the user, then call this tool again with the amount in the checkout_url or use pay_crypto(to=${to}, amount=<confirmed_amount>).`,
|
|
661
|
+
}, null, 2),
|
|
662
|
+
}],
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
// Call backend WDK transfer API
|
|
666
|
+
let amount = eip681_amount ?? 0;
|
|
667
|
+
// Decode ERC-20 transfer amount from calldata if EIP-681 amount not available
|
|
668
|
+
if (!amount && data && data.length >= 138) {
|
|
669
|
+
const amountHex = data.slice(-64);
|
|
670
|
+
const amountRaw = BigInt(`0x${amountHex}`);
|
|
671
|
+
amount = Number(amountRaw) / 1_000_000;
|
|
672
|
+
}
|
|
673
|
+
// Guard: if we still can't determine amount, ask user
|
|
674
|
+
if (!amount || amount <= 0) {
|
|
675
|
+
return {
|
|
676
|
+
content: [{
|
|
677
|
+
type: "text",
|
|
678
|
+
text: JSON.stringify({
|
|
679
|
+
route: "WEB3",
|
|
680
|
+
detected: true,
|
|
681
|
+
status: "AMOUNT_REQUIRED",
|
|
682
|
+
recipient: to,
|
|
683
|
+
message: `Web3 tx detected but could not decode USDT amount from calldata. Please confirm the exact amount with the user and use pay_crypto(to=${to}, amount=<amount>).`,
|
|
684
|
+
}, null, 2),
|
|
685
|
+
}],
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
const resp = await fetch(`${ZZERO_API}/api/wdk/transfer`, {
|
|
689
|
+
method: "POST",
|
|
690
|
+
headers: {
|
|
691
|
+
"Content-Type": "application/json",
|
|
692
|
+
"x-passport-key": API_KEY,
|
|
693
|
+
},
|
|
694
|
+
body: JSON.stringify({ to, amount, card_alias }),
|
|
695
|
+
});
|
|
696
|
+
const txResult = await resp.json();
|
|
697
|
+
if (!resp.ok || !txResult.success) {
|
|
698
|
+
return {
|
|
699
|
+
content: [{
|
|
700
|
+
type: "text",
|
|
701
|
+
text: JSON.stringify({
|
|
702
|
+
route: "WEB3",
|
|
703
|
+
status: "FAILED",
|
|
704
|
+
reason: txResult.message || txResult.error || "Unknown error from WDK transfer API",
|
|
705
|
+
}, null, 2),
|
|
706
|
+
}],
|
|
707
|
+
isError: true,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
return {
|
|
711
|
+
content: [{
|
|
712
|
+
type: "text",
|
|
713
|
+
text: JSON.stringify({
|
|
714
|
+
route: "WEB3",
|
|
715
|
+
method: web3Result.method,
|
|
716
|
+
status: "SUCCESS",
|
|
717
|
+
recipient: to,
|
|
718
|
+
amount_usdt: amount,
|
|
719
|
+
tx_hash: txResult.txHash,
|
|
720
|
+
message: `✅ Web3 payment sent on-chain! ${amount} USDT → ${to}. Tx: ${txResult.txHash}`,
|
|
721
|
+
gas_savings: "~$0.001 (ERC-4337 Paymaster, gasless for user)",
|
|
722
|
+
}, null, 2),
|
|
723
|
+
}],
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
// ── SCENARIO B: No Web3 — Fiat JIT Card fallback ────────────────────
|
|
727
|
+
console.error(`[SMART ROUTE] No Web3 detected. Scanning DOM for total price...`);
|
|
728
|
+
// ✅ BUG 1+2 FIX: Use try/finally so browser ALWAYS closes (even on success).
|
|
729
|
+
// fillCheckoutForm opens its own browser — this avoids triple browser + leak.
|
|
730
|
+
let totalPrice = null;
|
|
731
|
+
const priceBrowser = await playwright_1.chromium.launch({ headless: true });
|
|
732
|
+
try {
|
|
733
|
+
const pricePage = await priceBrowser.newPage();
|
|
734
|
+
await pricePage.goto(checkout_url, { waitUntil: "domcontentloaded", timeout: 20_000 });
|
|
735
|
+
totalPrice = await (0, extract_total_price_js_1.extractTotalPrice)(pricePage);
|
|
736
|
+
}
|
|
737
|
+
catch {
|
|
738
|
+
// ignore — totalPrice stays null, handled below
|
|
739
|
+
}
|
|
740
|
+
finally {
|
|
741
|
+
await priceBrowser.close().catch(() => { }); // ✅ Always close
|
|
742
|
+
}
|
|
743
|
+
if (!totalPrice) {
|
|
744
|
+
return {
|
|
745
|
+
content: [{
|
|
746
|
+
type: "text",
|
|
747
|
+
text: JSON.stringify({
|
|
748
|
+
route: "FIAT",
|
|
749
|
+
status: "PRICE_NOT_FOUND",
|
|
750
|
+
message: "Could not automatically detect the total price on this page. Please manually confirm the exact checkout price and use request_payment_token + execute_payment instead.",
|
|
751
|
+
}, null, 2),
|
|
752
|
+
}],
|
|
753
|
+
isError: true,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
if (totalPrice < 1 || totalPrice > 100) {
|
|
757
|
+
return {
|
|
758
|
+
content: [{
|
|
759
|
+
type: "text",
|
|
760
|
+
text: JSON.stringify({
|
|
761
|
+
route: "FIAT",
|
|
762
|
+
status: "AMOUNT_OUT_OF_RANGE",
|
|
763
|
+
detected_price: totalPrice,
|
|
764
|
+
message: `Detected price $${totalPrice} is outside the JIT card limit ($1-$100). Please confirm with user and use request_payment_token manually.`,
|
|
765
|
+
}, null, 2),
|
|
766
|
+
}],
|
|
767
|
+
isError: true,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
// Issue JIT card for exactly the detected total
|
|
771
|
+
console.error(`[SMART ROUTE] Fiat route: issuing JIT card for $${totalPrice}`);
|
|
772
|
+
const token = await issueTokenRemote(card_alias, totalPrice, checkout_url);
|
|
773
|
+
if (!token || token.error) {
|
|
774
|
+
return {
|
|
775
|
+
content: [{
|
|
776
|
+
type: "text",
|
|
777
|
+
text: JSON.stringify({
|
|
778
|
+
route: "FIAT",
|
|
779
|
+
status: "TOKEN_ISSUE_FAILED",
|
|
780
|
+
message: token?.message || "Could not issue JIT card. Check balance or card alias.",
|
|
781
|
+
}, null, 2),
|
|
782
|
+
}],
|
|
783
|
+
isError: true,
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
// Resolve token and auto-fill form
|
|
787
|
+
const cardData = await resolveTokenRemote(token.token);
|
|
788
|
+
if (!cardData || cardData.error) {
|
|
789
|
+
return {
|
|
790
|
+
content: [{
|
|
791
|
+
type: "text",
|
|
792
|
+
text: JSON.stringify({
|
|
793
|
+
route: "FIAT",
|
|
794
|
+
status: "CARD_RESOLVE_FAILED",
|
|
795
|
+
message: "Failed to resolve JIT card data. Token may have expired.",
|
|
796
|
+
}, null, 2),
|
|
797
|
+
}],
|
|
798
|
+
isError: true,
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
const fillResult = await (0, playwright_bridge_js_1.fillCheckoutForm)(checkout_url, cardData);
|
|
802
|
+
if (fillResult.success) {
|
|
803
|
+
await burnTokenRemote(token.token);
|
|
804
|
+
}
|
|
805
|
+
return {
|
|
806
|
+
content: [{
|
|
807
|
+
type: "text",
|
|
808
|
+
text: JSON.stringify({
|
|
809
|
+
route: "FIAT",
|
|
810
|
+
status: fillResult.success ? "SUCCESS" : "FILL_FAILED",
|
|
811
|
+
detected_price: totalPrice,
|
|
812
|
+
jit_card_issued: true,
|
|
813
|
+
message: fillResult.success
|
|
814
|
+
? `✅ JIT Visa card issued for $${totalPrice} and checkout filled successfully.`
|
|
815
|
+
: `❌ JIT card issued but checkout form fill failed: ${fillResult.message}`,
|
|
816
|
+
receipt_id: fillResult.receipt_id || null,
|
|
817
|
+
}, null, 2),
|
|
818
|
+
}],
|
|
819
|
+
isError: !fillResult.success,
|
|
820
|
+
};
|
|
821
|
+
});
|
|
822
|
+
// ============================================================
|
|
375
823
|
// RESOURCE: Z-ZERO Autonomous Payment SOP
|
|
376
824
|
// ============================================================
|
|
377
825
|
server.resource("Standard Operating Procedure (SOP) for Autonomous Payments", "mcp://resources/sop", {
|
|
@@ -426,8 +874,8 @@ When asked to make a purchase, execute the following steps precisely in order:
|
|
|
426
874
|
async function main() {
|
|
427
875
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
428
876
|
await server.connect(transport);
|
|
429
|
-
console.error("🔐 OpenClaw MCP Server
|
|
877
|
+
console.error("🔐 OpenClaw MCP Server v2.0.0 running (Phase 2: Smart Routing enabled)...");
|
|
430
878
|
console.error("Status: Secure & Connected to Z-ZERO Gateway");
|
|
431
|
-
console.error("Tools: list_cards, check_balance, request_payment_token, execute_payment, cancel_payment_token, request_human_approval");
|
|
879
|
+
console.error("Tools: list_cards, check_balance, request_payment_token, execute_payment, cancel_payment_token, request_human_approval, auto_pay_checkout");
|
|
432
880
|
}
|
|
433
881
|
main().catch(console.error);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Page } from "playwright";
|
|
2
|
+
/**
|
|
3
|
+
* Multi-strategy total price extractor.
|
|
4
|
+
* Strategy order (most reliable → least reliable):
|
|
5
|
+
* 1. Known CSS selectors for major platforms (Shopify, Stripe, Woo)
|
|
6
|
+
* 2. Aria / data-testid patterns
|
|
7
|
+
* 3. Text heuristic: find largest $ amount near keywords "total", "amount due"
|
|
8
|
+
*/
|
|
9
|
+
export declare function extractTotalPrice(page: Page): Promise<number | null>;
|