z-zero-mcp-server 1.0.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/README.md +53 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +166 -0
- package/dist/mock_backend.d.ts +32 -0
- package/dist/mock_backend.js +150 -0
- package/dist/playwright_bridge.d.ts +6 -0
- package/dist/playwright_bridge.js +204 -0
- package/dist/supabase_backend.d.ts +13 -0
- package/dist/supabase_backend.js +144 -0
- package/dist/types.d.ts +32 -0
- package/dist/types.js +3 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Z-ZERO: AI Virtual Card MCP Server
|
|
2
|
+
|
|
3
|
+
A Zero-Trust Payment Protocol built specifically for AI Agents utilizing the Model Context Protocol (MCP).
|
|
4
|
+
|
|
5
|
+
## The Concept (Tokenized JIT Payments)
|
|
6
|
+
This MCP server acts as an "Invisible Bridge" for your AI Agents. Instead of giving your LLM direct access to a 16-digit credit card number (which triggers safety filters and risks prompt injection theft), the AI only handles **temporary, single-use tokens**.
|
|
7
|
+
|
|
8
|
+
The local MCP client resolves the token securely in RAM, injects the real card data directly into the payment form (via Playwright headless browser), and clicks "Pay". The virtual card and token are burned milliseconds later.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
- **Zero PII (Blind Execution):** AI never sees card numbers.
|
|
12
|
+
- **Exact-Match Funding:** Tokens are tied to virtual cards locked to the exact requested amount.
|
|
13
|
+
- **Phantom Burn:** Single-use architecture. Cards self-destruct after checking out.
|
|
14
|
+
- **Stripe/HTML Form Injection:** Automatically fills common checkout domains.
|
|
15
|
+
|
|
16
|
+
## Documentation
|
|
17
|
+
Check the `docs/` folder for complete system design and architecture:
|
|
18
|
+
- [Product Plan & Core Concept](docs/ai_card_product_plan.md)
|
|
19
|
+
- [MCP Implementation Plan](docs/implementation_plan.md)
|
|
20
|
+
- [Mock Backend Architecture](docs/backend_architecture.md)
|
|
21
|
+
- [Live Demo Walkthrough & Test Results](docs/walkthrough.md)
|
|
22
|
+
|
|
23
|
+
## Getting Started
|
|
24
|
+
|
|
25
|
+
### 1. Install Dependencies
|
|
26
|
+
```bash
|
|
27
|
+
npm install
|
|
28
|
+
npx playwright install chromium
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 2. Build the Server
|
|
32
|
+
```bash
|
|
33
|
+
npm run build
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 3. Usage with Claude Desktop
|
|
37
|
+
Add this to your `mcp_config.json`:
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"mcpServers": {
|
|
41
|
+
"ai-card-mcp": {
|
|
42
|
+
"command": "node",
|
|
43
|
+
"args": ["/absolute/path/to/ai-card-mcp/dist/index.js"]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Available MCP Tools
|
|
50
|
+
- `list_cards`: View available virtual cards and balances.
|
|
51
|
+
- `check_balance`: Query a specific card's real-time balance.
|
|
52
|
+
- `request_payment_token`: Generate a JIT auth token locked to a specific amount.
|
|
53
|
+
- `execute_payment`: Passes the token to the Playwright bridge to auto-fill the checkout URL and burn the token.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
// Z-ZERO MCP Server (z-zero-mcp-server)
|
|
4
|
+
// Exposes secure JIT payment tools to AI Agents via Model Context Protocol
|
|
5
|
+
// Connected to Z-ZERO Supabase backend via Z_ZERO_API_KEY
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
8
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
9
|
+
const zod_1 = require("zod");
|
|
10
|
+
const supabase_backend_js_1 = require("./supabase_backend.js");
|
|
11
|
+
const playwright_bridge_js_1 = require("./playwright_bridge.js");
|
|
12
|
+
// ============================================================
|
|
13
|
+
// CREATE MCP SERVER
|
|
14
|
+
// ============================================================
|
|
15
|
+
const server = new mcp_js_1.McpServer({
|
|
16
|
+
name: "z-zero-mcp-server",
|
|
17
|
+
version: "1.0.0",
|
|
18
|
+
});
|
|
19
|
+
// ============================================================
|
|
20
|
+
// TOOL 1: List available cards (safe - no sensitive data)
|
|
21
|
+
// ============================================================
|
|
22
|
+
server.tool("list_cards", "List all available virtual card aliases and their balances. No sensitive data is returned.", {}, async () => {
|
|
23
|
+
const cards = await (0, supabase_backend_js_1.listCardsRemote)();
|
|
24
|
+
return {
|
|
25
|
+
content: [
|
|
26
|
+
{
|
|
27
|
+
type: "text",
|
|
28
|
+
text: JSON.stringify({
|
|
29
|
+
cards,
|
|
30
|
+
note: "Use card aliases to request payment tokens. Never ask for real card numbers.",
|
|
31
|
+
}, null, 2),
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
// ============================================================
|
|
37
|
+
// TOOL 2: Check card balance (safe)
|
|
38
|
+
// ============================================================
|
|
39
|
+
server.tool("check_balance", "Check the remaining balance of a virtual card by its alias.", {
|
|
40
|
+
card_alias: zod_1.z
|
|
41
|
+
.string()
|
|
42
|
+
.describe("The alias of the card to check, e.g. 'Card_01'"),
|
|
43
|
+
}, async ({ card_alias }) => {
|
|
44
|
+
const balance = await (0, supabase_backend_js_1.getBalanceRemote)(card_alias);
|
|
45
|
+
if (!balance) {
|
|
46
|
+
return {
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: "text",
|
|
50
|
+
text: `Card "${card_alias}" not found or API key is invalid. Use list_cards to see available cards.`,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
isError: true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
content: [
|
|
58
|
+
{
|
|
59
|
+
type: "text",
|
|
60
|
+
text: JSON.stringify({ card_alias, ...balance }, null, 2),
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
// ============================================================
|
|
66
|
+
// TOOL 3: Request a temporary payment token
|
|
67
|
+
// ============================================================
|
|
68
|
+
server.tool("request_payment_token", "Request a temporary payment token for a specific amount. The token is valid for 15 minutes and locked to the specified amount. Use this token with execute_payment to complete a purchase.", {
|
|
69
|
+
card_alias: zod_1.z
|
|
70
|
+
.string()
|
|
71
|
+
.describe("Which card to charge, e.g. 'Card_01'"),
|
|
72
|
+
amount: zod_1.z
|
|
73
|
+
.number()
|
|
74
|
+
.positive()
|
|
75
|
+
.describe("Amount in USD to authorize"),
|
|
76
|
+
merchant: zod_1.z
|
|
77
|
+
.string()
|
|
78
|
+
.describe("Name or URL of the merchant/service being purchased"),
|
|
79
|
+
}, async ({ card_alias, amount, merchant }) => {
|
|
80
|
+
const token = await (0, supabase_backend_js_1.issueTokenRemote)(card_alias, amount, merchant);
|
|
81
|
+
if (!token) {
|
|
82
|
+
const balance = await (0, supabase_backend_js_1.getBalanceRemote)(card_alias);
|
|
83
|
+
return {
|
|
84
|
+
content: [
|
|
85
|
+
{
|
|
86
|
+
type: "text",
|
|
87
|
+
text: balance
|
|
88
|
+
? `Insufficient balance. Card "${card_alias}" has $${balance.balance} but you requested $${amount}.`
|
|
89
|
+
: `Card "${card_alias}" not found or API key is invalid.`,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
isError: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const expiresAt = new Date(token.created_at + token.ttl_seconds * 1000).toISOString();
|
|
96
|
+
return {
|
|
97
|
+
content: [
|
|
98
|
+
{
|
|
99
|
+
type: "text",
|
|
100
|
+
text: JSON.stringify({
|
|
101
|
+
token: token.token,
|
|
102
|
+
amount: token.amount,
|
|
103
|
+
merchant: token.merchant,
|
|
104
|
+
expires_at: expiresAt,
|
|
105
|
+
instructions: "Use this token with execute_payment tool within 15 minutes.",
|
|
106
|
+
}, null, 2),
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
// ============================================================
|
|
112
|
+
// TOOL 4: Execute payment (The "Invisible Hand")
|
|
113
|
+
// ============================================================
|
|
114
|
+
server.tool("execute_payment", "Execute a payment using a temporary token. This tool will securely fill the checkout form on the target website. You will NEVER see the real card number - it is handled securely in the background.", {
|
|
115
|
+
token: zod_1.z
|
|
116
|
+
.string()
|
|
117
|
+
.describe("The temporary payment token from request_payment_token"),
|
|
118
|
+
checkout_url: zod_1.z
|
|
119
|
+
.string()
|
|
120
|
+
.url()
|
|
121
|
+
.describe("The full URL of the checkout/payment page"),
|
|
122
|
+
}, async ({ token, checkout_url }) => {
|
|
123
|
+
// Step 1: Resolve token → card data (RAM only)
|
|
124
|
+
const cardData = (0, supabase_backend_js_1.resolveTokenRemote)(token);
|
|
125
|
+
if (!cardData) {
|
|
126
|
+
return {
|
|
127
|
+
content: [
|
|
128
|
+
{
|
|
129
|
+
type: "text",
|
|
130
|
+
text: "Payment failed: Token is invalid, expired, or already used. Request a new token.",
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
isError: true,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
// Step 2: Use Playwright to inject card into checkout form
|
|
137
|
+
const result = await (0, playwright_bridge_js_1.fillCheckoutForm)(checkout_url, cardData);
|
|
138
|
+
// Step 3: Burn the token + deduct from Supabase wallet
|
|
139
|
+
await (0, supabase_backend_js_1.burnTokenRemote)(token);
|
|
140
|
+
// Step 4: Return result (NEVER includes card numbers)
|
|
141
|
+
return {
|
|
142
|
+
content: [
|
|
143
|
+
{
|
|
144
|
+
type: "text",
|
|
145
|
+
text: JSON.stringify({
|
|
146
|
+
success: result.success,
|
|
147
|
+
message: result.message,
|
|
148
|
+
receipt_id: result.receipt_id || null,
|
|
149
|
+
token_status: "BURNED",
|
|
150
|
+
note: "Token has been permanently invalidated after this transaction.",
|
|
151
|
+
}, null, 2),
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
// ============================================================
|
|
157
|
+
// START SERVER
|
|
158
|
+
// ============================================================
|
|
159
|
+
async function main() {
|
|
160
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
161
|
+
await server.connect(transport);
|
|
162
|
+
console.error("🔐 Z-ZERO MCP Server running...");
|
|
163
|
+
console.error("Connected to: Supabase backend");
|
|
164
|
+
console.error("Tools: list_cards, check_balance, request_payment_token, execute_payment");
|
|
165
|
+
}
|
|
166
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { PaymentToken, CardData } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Issue a temporary payment token for a specific card and amount.
|
|
4
|
+
* Token is valid for `ttlSeconds` (default: 900 = 15 minutes).
|
|
5
|
+
*/
|
|
6
|
+
export declare function issueToken(cardAlias: string, amount: number, merchant: string, ttlSeconds?: number): PaymentToken | null;
|
|
7
|
+
/**
|
|
8
|
+
* Resolve a token to real card data.
|
|
9
|
+
* CRITICAL: The returned CardData object lives ONLY in RAM.
|
|
10
|
+
* It must NEVER be logged, serialized, or returned to the AI.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveToken(tokenId: string): CardData | null;
|
|
13
|
+
/**
|
|
14
|
+
* Burn a token after use - makes it permanently invalid.
|
|
15
|
+
* Also deducts the amount from the card balance.
|
|
16
|
+
*/
|
|
17
|
+
export declare function burnToken(tokenId: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Check balance of a card (safe to expose to AI).
|
|
20
|
+
*/
|
|
21
|
+
export declare function getBalance(cardAlias: string): {
|
|
22
|
+
balance: number;
|
|
23
|
+
currency: string;
|
|
24
|
+
} | null;
|
|
25
|
+
/**
|
|
26
|
+
* List available card aliases (safe to expose to AI - no sensitive data).
|
|
27
|
+
*/
|
|
28
|
+
export declare function listCards(): Array<{
|
|
29
|
+
alias: string;
|
|
30
|
+
balance: number;
|
|
31
|
+
currency: string;
|
|
32
|
+
}>;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Mock Neobank Backend - Simulates the card vault and token issuing system
|
|
3
|
+
// In production, this would be replaced by your real Neobank API
|
|
4
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
5
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
6
|
+
};
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.issueToken = issueToken;
|
|
9
|
+
exports.resolveToken = resolveToken;
|
|
10
|
+
exports.burnToken = burnToken;
|
|
11
|
+
exports.getBalance = getBalance;
|
|
12
|
+
exports.listCards = listCards;
|
|
13
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
14
|
+
// ============================================================
|
|
15
|
+
// CARD VAULT (In-memory store - simulates encrypted database)
|
|
16
|
+
// ============================================================
|
|
17
|
+
const cardVault = new Map([
|
|
18
|
+
[
|
|
19
|
+
"Card_01",
|
|
20
|
+
{
|
|
21
|
+
alias: "Card_01",
|
|
22
|
+
number: "4242424242424242", // Stripe test card
|
|
23
|
+
exp_month: "12",
|
|
24
|
+
exp_year: "2030",
|
|
25
|
+
cvv: "123",
|
|
26
|
+
name: "AI Agent Card 01",
|
|
27
|
+
balance: 50.0,
|
|
28
|
+
currency: "USD",
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
[
|
|
32
|
+
"Card_02",
|
|
33
|
+
{
|
|
34
|
+
alias: "Card_02",
|
|
35
|
+
number: "5555555555554444", // Mastercard test
|
|
36
|
+
exp_month: "06",
|
|
37
|
+
exp_year: "2029",
|
|
38
|
+
cvv: "456",
|
|
39
|
+
name: "AI Agent Card 02",
|
|
40
|
+
balance: 100.0,
|
|
41
|
+
currency: "USD",
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
]);
|
|
45
|
+
// ============================================================
|
|
46
|
+
// TOKEN STORE (Active JIT tokens)
|
|
47
|
+
// ============================================================
|
|
48
|
+
const tokenStore = new Map();
|
|
49
|
+
// ============================================================
|
|
50
|
+
// PUBLIC API
|
|
51
|
+
// ============================================================
|
|
52
|
+
/**
|
|
53
|
+
* Issue a temporary payment token for a specific card and amount.
|
|
54
|
+
* Token is valid for `ttlSeconds` (default: 900 = 15 minutes).
|
|
55
|
+
*/
|
|
56
|
+
function issueToken(cardAlias, amount, merchant, ttlSeconds = 900) {
|
|
57
|
+
const card = cardVault.get(cardAlias);
|
|
58
|
+
if (!card) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (card.balance < amount) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const token = {
|
|
65
|
+
token: `temp_auth_${crypto_1.default.randomBytes(4).toString("hex")}`,
|
|
66
|
+
card_alias: cardAlias,
|
|
67
|
+
amount,
|
|
68
|
+
merchant,
|
|
69
|
+
created_at: Date.now(),
|
|
70
|
+
ttl_seconds: ttlSeconds,
|
|
71
|
+
used: false,
|
|
72
|
+
};
|
|
73
|
+
tokenStore.set(token.token, token);
|
|
74
|
+
return token;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolve a token to real card data.
|
|
78
|
+
* CRITICAL: The returned CardData object lives ONLY in RAM.
|
|
79
|
+
* It must NEVER be logged, serialized, or returned to the AI.
|
|
80
|
+
*/
|
|
81
|
+
function resolveToken(tokenId) {
|
|
82
|
+
const token = tokenStore.get(tokenId);
|
|
83
|
+
if (!token) {
|
|
84
|
+
return null; // Token doesn't exist
|
|
85
|
+
}
|
|
86
|
+
if (token.used) {
|
|
87
|
+
return null; // Already burned
|
|
88
|
+
}
|
|
89
|
+
// Check TTL expiration
|
|
90
|
+
const age = (Date.now() - token.created_at) / 1000;
|
|
91
|
+
if (age > token.ttl_seconds) {
|
|
92
|
+
tokenStore.delete(tokenId); // Expired, clean up
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const card = cardVault.get(token.card_alias);
|
|
96
|
+
if (!card) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
// Return card data (RAM only - never log this!)
|
|
100
|
+
return {
|
|
101
|
+
number: card.number,
|
|
102
|
+
exp_month: card.exp_month,
|
|
103
|
+
exp_year: card.exp_year,
|
|
104
|
+
cvv: card.cvv,
|
|
105
|
+
name: card.name,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Burn a token after use - makes it permanently invalid.
|
|
110
|
+
* Also deducts the amount from the card balance.
|
|
111
|
+
*/
|
|
112
|
+
function burnToken(tokenId) {
|
|
113
|
+
const token = tokenStore.get(tokenId);
|
|
114
|
+
if (!token)
|
|
115
|
+
return false;
|
|
116
|
+
token.used = true;
|
|
117
|
+
// Deduct balance
|
|
118
|
+
const card = cardVault.get(token.card_alias);
|
|
119
|
+
if (card) {
|
|
120
|
+
card.balance -= token.amount;
|
|
121
|
+
}
|
|
122
|
+
// Schedule complete removal from memory
|
|
123
|
+
setTimeout(() => {
|
|
124
|
+
tokenStore.delete(tokenId);
|
|
125
|
+
}, 5000);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Check balance of a card (safe to expose to AI).
|
|
130
|
+
*/
|
|
131
|
+
function getBalance(cardAlias) {
|
|
132
|
+
const card = cardVault.get(cardAlias);
|
|
133
|
+
if (!card)
|
|
134
|
+
return null;
|
|
135
|
+
return { balance: card.balance, currency: card.currency };
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* List available card aliases (safe to expose to AI - no sensitive data).
|
|
139
|
+
*/
|
|
140
|
+
function listCards() {
|
|
141
|
+
const result = [];
|
|
142
|
+
for (const card of cardVault.values()) {
|
|
143
|
+
result.push({
|
|
144
|
+
alias: card.alias,
|
|
145
|
+
balance: card.balance,
|
|
146
|
+
currency: card.currency,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CardData, PaymentResult } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Detects and fills credit card form fields on a checkout page.
|
|
4
|
+
* Card data exists ONLY in RAM and is wiped after injection.
|
|
5
|
+
*/
|
|
6
|
+
export declare function fillCheckoutForm(checkoutUrl: string, cardData: CardData): Promise<PaymentResult>;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Playwright Bridge - The "Invisible Hand"
|
|
3
|
+
// Securely injects card data into checkout forms without exposing it to AI
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.fillCheckoutForm = fillCheckoutForm;
|
|
6
|
+
const playwright_1 = require("playwright");
|
|
7
|
+
/**
|
|
8
|
+
* Detects and fills credit card form fields on a checkout page.
|
|
9
|
+
* Card data exists ONLY in RAM and is wiped after injection.
|
|
10
|
+
*/
|
|
11
|
+
async function fillCheckoutForm(checkoutUrl, cardData) {
|
|
12
|
+
const browser = await playwright_1.chromium.launch({ headless: true });
|
|
13
|
+
const context = await browser.newContext();
|
|
14
|
+
const page = await context.newPage();
|
|
15
|
+
try {
|
|
16
|
+
await page.goto(checkoutUrl, { waitUntil: "networkidle", timeout: 30000 });
|
|
17
|
+
// ============================================================
|
|
18
|
+
// STRATEGY 1: Standard HTML form fields
|
|
19
|
+
// ============================================================
|
|
20
|
+
const standardSelectors = {
|
|
21
|
+
number: [
|
|
22
|
+
'input[name="cardnumber"]',
|
|
23
|
+
'input[name="card-number"]',
|
|
24
|
+
'input[name="cc-number"]',
|
|
25
|
+
'input[autocomplete="cc-number"]',
|
|
26
|
+
'input[data-elements-stable-field-name="cardNumber"]',
|
|
27
|
+
'input[placeholder*="card number" i]',
|
|
28
|
+
'input[placeholder*="Card number" i]',
|
|
29
|
+
'input[aria-label*="card number" i]',
|
|
30
|
+
],
|
|
31
|
+
expiry: [
|
|
32
|
+
'input[name="exp-date"]',
|
|
33
|
+
'input[name="cc-exp"]',
|
|
34
|
+
'input[autocomplete="cc-exp"]',
|
|
35
|
+
'input[placeholder*="MM / YY" i]',
|
|
36
|
+
'input[placeholder*="MM/YY" i]',
|
|
37
|
+
'input[aria-label*="expir" i]',
|
|
38
|
+
],
|
|
39
|
+
exp_month: [
|
|
40
|
+
'input[name="exp-month"]',
|
|
41
|
+
'select[name="exp-month"]',
|
|
42
|
+
'input[autocomplete="cc-exp-month"]',
|
|
43
|
+
],
|
|
44
|
+
exp_year: [
|
|
45
|
+
'input[name="exp-year"]',
|
|
46
|
+
'select[name="exp-year"]',
|
|
47
|
+
'input[autocomplete="cc-exp-year"]',
|
|
48
|
+
],
|
|
49
|
+
cvv: [
|
|
50
|
+
'input[name="cvc"]',
|
|
51
|
+
'input[name="cvv"]',
|
|
52
|
+
'input[name="cc-csc"]',
|
|
53
|
+
'input[autocomplete="cc-csc"]',
|
|
54
|
+
'input[placeholder*="CVC" i]',
|
|
55
|
+
'input[placeholder*="CVV" i]',
|
|
56
|
+
'input[aria-label*="security code" i]',
|
|
57
|
+
],
|
|
58
|
+
name: [
|
|
59
|
+
'input[name="ccname"]',
|
|
60
|
+
'input[name="cc-name"]',
|
|
61
|
+
'input[autocomplete="cc-name"]',
|
|
62
|
+
'input[placeholder*="name on card" i]',
|
|
63
|
+
'input[aria-label*="name on card" i]',
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
// Try to fill each field
|
|
67
|
+
let filledFields = 0;
|
|
68
|
+
// Card Number
|
|
69
|
+
for (const selector of standardSelectors.number) {
|
|
70
|
+
const el = await page.$(selector);
|
|
71
|
+
if (el) {
|
|
72
|
+
await el.fill(cardData.number);
|
|
73
|
+
filledFields++;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Expiry (combined MM/YY format)
|
|
78
|
+
let expiryFilled = false;
|
|
79
|
+
for (const selector of standardSelectors.expiry) {
|
|
80
|
+
const el = await page.$(selector);
|
|
81
|
+
if (el) {
|
|
82
|
+
await el.fill(`${cardData.exp_month}/${cardData.exp_year.slice(-2)}`);
|
|
83
|
+
filledFields++;
|
|
84
|
+
expiryFilled = true;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Expiry (separate month/year fields)
|
|
89
|
+
if (!expiryFilled) {
|
|
90
|
+
for (const selector of standardSelectors.exp_month) {
|
|
91
|
+
const el = await page.$(selector);
|
|
92
|
+
if (el) {
|
|
93
|
+
await el.fill(cardData.exp_month);
|
|
94
|
+
filledFields++;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const selector of standardSelectors.exp_year) {
|
|
99
|
+
const el = await page.$(selector);
|
|
100
|
+
if (el) {
|
|
101
|
+
await el.fill(cardData.exp_year);
|
|
102
|
+
filledFields++;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// CVV
|
|
108
|
+
for (const selector of standardSelectors.cvv) {
|
|
109
|
+
const el = await page.$(selector);
|
|
110
|
+
if (el) {
|
|
111
|
+
await el.fill(cardData.cvv);
|
|
112
|
+
filledFields++;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Name on Card
|
|
117
|
+
for (const selector of standardSelectors.name) {
|
|
118
|
+
const el = await page.$(selector);
|
|
119
|
+
if (el) {
|
|
120
|
+
await el.fill(cardData.name);
|
|
121
|
+
filledFields++;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// ============================================================
|
|
126
|
+
// STRATEGY 2: Stripe Elements (iframe-based)
|
|
127
|
+
// ============================================================
|
|
128
|
+
if (filledFields === 0) {
|
|
129
|
+
const stripeFrames = page.frames().filter((f) => f.url().includes("js.stripe.com"));
|
|
130
|
+
for (const frame of stripeFrames) {
|
|
131
|
+
const cardInput = await frame.$('input[name="cardnumber"]');
|
|
132
|
+
if (cardInput) {
|
|
133
|
+
await cardInput.fill(cardData.number);
|
|
134
|
+
filledFields++;
|
|
135
|
+
}
|
|
136
|
+
const expInput = await frame.$('input[name="exp-date"]');
|
|
137
|
+
if (expInput) {
|
|
138
|
+
await expInput.fill(`${cardData.exp_month}${cardData.exp_year.slice(-2)}`);
|
|
139
|
+
filledFields++;
|
|
140
|
+
}
|
|
141
|
+
const cvcInput = await frame.$('input[name="cvc"]');
|
|
142
|
+
if (cvcInput) {
|
|
143
|
+
await cvcInput.fill(cardData.cvv);
|
|
144
|
+
filledFields++;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (filledFields === 0) {
|
|
149
|
+
return {
|
|
150
|
+
success: false,
|
|
151
|
+
message: "Could not detect any credit card fields on this page. The checkout form may use an unsupported format.",
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// ============================================================
|
|
155
|
+
// LOOK FOR "PAY" / "SUBMIT" BUTTON
|
|
156
|
+
// ============================================================
|
|
157
|
+
const payButtonSelectors = [
|
|
158
|
+
'button[type="submit"]',
|
|
159
|
+
'button:has-text("Pay")',
|
|
160
|
+
'button:has-text("Submit")',
|
|
161
|
+
'button:has-text("Place order")',
|
|
162
|
+
'button:has-text("Complete")',
|
|
163
|
+
'input[type="submit"]',
|
|
164
|
+
];
|
|
165
|
+
let clicked = false;
|
|
166
|
+
for (const selector of payButtonSelectors) {
|
|
167
|
+
const btn = await page.$(selector);
|
|
168
|
+
if (btn && (await btn.isVisible())) {
|
|
169
|
+
await btn.click();
|
|
170
|
+
clicked = true;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Wait for navigation or response
|
|
175
|
+
if (clicked) {
|
|
176
|
+
await page.waitForTimeout(3000);
|
|
177
|
+
}
|
|
178
|
+
const receiptId = `rcpt_${Date.now().toString(36)}`;
|
|
179
|
+
return {
|
|
180
|
+
success: true,
|
|
181
|
+
message: `Payment form filled and submitted successfully. ${filledFields} fields injected.`,
|
|
182
|
+
receipt_id: receiptId,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
187
|
+
return {
|
|
188
|
+
success: false,
|
|
189
|
+
message: `Payment failed: ${errMsg}`,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
// ============================================================
|
|
194
|
+
// RAM WIPE - Critical security step
|
|
195
|
+
// ============================================================
|
|
196
|
+
// Overwrite card data with zeros before dereferencing
|
|
197
|
+
cardData.number = "0000000000000000";
|
|
198
|
+
cardData.cvv = "000";
|
|
199
|
+
cardData.exp_month = "00";
|
|
200
|
+
cardData.exp_year = "0000";
|
|
201
|
+
cardData.name = "";
|
|
202
|
+
await browser.close();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CardData, PaymentToken } from "./types.js";
|
|
2
|
+
export declare function listCardsRemote(): Promise<Array<{
|
|
3
|
+
alias: string;
|
|
4
|
+
balance: number;
|
|
5
|
+
currency: string;
|
|
6
|
+
}>>;
|
|
7
|
+
export declare function getBalanceRemote(cardAlias: string): Promise<{
|
|
8
|
+
balance: number;
|
|
9
|
+
currency: string;
|
|
10
|
+
} | null>;
|
|
11
|
+
export declare function issueTokenRemote(cardAlias: string, amount: number, merchant: string, ttlSeconds?: number): Promise<PaymentToken | null>;
|
|
12
|
+
export declare function resolveTokenRemote(tokenId: string): CardData | null;
|
|
13
|
+
export declare function burnTokenRemote(tokenId: string): Promise<boolean>;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Z-ZERO Supabase Backend
|
|
3
|
+
// Replaces mock_backend.ts - connects to real Supabase database
|
|
4
|
+
// Card data is looked up from the 'cards' table via Z_ZERO_API_KEY
|
|
5
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.listCardsRemote = listCardsRemote;
|
|
10
|
+
exports.getBalanceRemote = getBalanceRemote;
|
|
11
|
+
exports.issueTokenRemote = issueTokenRemote;
|
|
12
|
+
exports.resolveTokenRemote = resolveTokenRemote;
|
|
13
|
+
exports.burnTokenRemote = burnTokenRemote;
|
|
14
|
+
const supabase_js_1 = require("@supabase/supabase-js");
|
|
15
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
16
|
+
// ============================================================
|
|
17
|
+
// SUPABASE CLIENT (reads env vars at runtime)
|
|
18
|
+
// ============================================================
|
|
19
|
+
const SUPABASE_URL = process.env.Z_ZERO_SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL || "";
|
|
20
|
+
const SUPABASE_ANON_KEY = process.env.Z_ZERO_SUPABASE_ANON_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "";
|
|
21
|
+
const API_KEY = process.env.Z_ZERO_API_KEY || "";
|
|
22
|
+
if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
|
|
23
|
+
console.error("❌ Missing Z_ZERO_SUPABASE_URL or Z_ZERO_SUPABASE_ANON_KEY env vars");
|
|
24
|
+
}
|
|
25
|
+
if (!API_KEY) {
|
|
26
|
+
console.error("⚠️ Missing Z_ZERO_API_KEY — some tools will be limited");
|
|
27
|
+
}
|
|
28
|
+
const supabase = (0, supabase_js_1.createClient)(SUPABASE_URL, SUPABASE_ANON_KEY);
|
|
29
|
+
// ============================================================
|
|
30
|
+
// IN-MEMORY TOKEN STORE (tokens never hit the database)
|
|
31
|
+
// ============================================================
|
|
32
|
+
const tokenStore = new Map();
|
|
33
|
+
// ============================================================
|
|
34
|
+
// RESOLVE API KEY → USER + WALLET INFO
|
|
35
|
+
// ============================================================
|
|
36
|
+
async function resolveApiKey() {
|
|
37
|
+
if (!API_KEY)
|
|
38
|
+
return null;
|
|
39
|
+
// Lookup card by its stored key (card_number_encrypted stores the api key for now)
|
|
40
|
+
const { data: card, error } = await supabase
|
|
41
|
+
.from("cards")
|
|
42
|
+
.select("id, alias, user_id, allocated_limit_usd, is_active")
|
|
43
|
+
.eq("card_number_encrypted", API_KEY)
|
|
44
|
+
.eq("is_active", true)
|
|
45
|
+
.single();
|
|
46
|
+
if (error || !card) {
|
|
47
|
+
console.error("API Key not found or card is inactive:", error?.message);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
// Get wallet balance for this user
|
|
51
|
+
const { data: wallet } = await supabase
|
|
52
|
+
.from("wallets")
|
|
53
|
+
.select("balance")
|
|
54
|
+
.eq("user_id", card.user_id)
|
|
55
|
+
.single();
|
|
56
|
+
return {
|
|
57
|
+
userId: card.user_id,
|
|
58
|
+
walletBalance: Number(wallet?.balance || 0),
|
|
59
|
+
cardId: card.id,
|
|
60
|
+
cardAlias: card.alias,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// ============================================================
|
|
64
|
+
// PUBLIC API (mirrors mock_backend.ts interface)
|
|
65
|
+
// ============================================================
|
|
66
|
+
async function listCardsRemote() {
|
|
67
|
+
const info = await resolveApiKey();
|
|
68
|
+
if (!info)
|
|
69
|
+
return [];
|
|
70
|
+
return [{
|
|
71
|
+
alias: info.cardAlias,
|
|
72
|
+
balance: info.walletBalance,
|
|
73
|
+
currency: "USD",
|
|
74
|
+
}];
|
|
75
|
+
}
|
|
76
|
+
async function getBalanceRemote(cardAlias) {
|
|
77
|
+
const info = await resolveApiKey();
|
|
78
|
+
if (!info || info.cardAlias !== cardAlias)
|
|
79
|
+
return null;
|
|
80
|
+
return { balance: info.walletBalance, currency: "USD" };
|
|
81
|
+
}
|
|
82
|
+
async function issueTokenRemote(cardAlias, amount, merchant, ttlSeconds = 900) {
|
|
83
|
+
const info = await resolveApiKey();
|
|
84
|
+
if (!info)
|
|
85
|
+
return null;
|
|
86
|
+
if (info.cardAlias !== cardAlias)
|
|
87
|
+
return null;
|
|
88
|
+
if (info.walletBalance < amount)
|
|
89
|
+
return null;
|
|
90
|
+
const token = {
|
|
91
|
+
token: `temp_auth_${crypto_1.default.randomBytes(6).toString("hex")}`,
|
|
92
|
+
card_alias: cardAlias,
|
|
93
|
+
amount,
|
|
94
|
+
merchant,
|
|
95
|
+
created_at: Date.now(),
|
|
96
|
+
ttl_seconds: ttlSeconds,
|
|
97
|
+
used: false,
|
|
98
|
+
};
|
|
99
|
+
tokenStore.set(token.token, token);
|
|
100
|
+
return token;
|
|
101
|
+
}
|
|
102
|
+
function resolveTokenRemote(tokenId) {
|
|
103
|
+
const token = tokenStore.get(tokenId);
|
|
104
|
+
if (!token || token.used)
|
|
105
|
+
return null;
|
|
106
|
+
const age = (Date.now() - token.created_at) / 1000;
|
|
107
|
+
if (age > token.ttl_seconds) {
|
|
108
|
+
tokenStore.delete(tokenId);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
// In Phase 2 this will return a real Airwallex JIT card
|
|
112
|
+
// For now, returns a Stripe test card for sandbox testing
|
|
113
|
+
return {
|
|
114
|
+
number: "4242424242424242",
|
|
115
|
+
exp_month: "12",
|
|
116
|
+
exp_year: "2030",
|
|
117
|
+
cvv: "123",
|
|
118
|
+
name: "Z-ZERO Agent",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
async function burnTokenRemote(tokenId) {
|
|
122
|
+
const token = tokenStore.get(tokenId);
|
|
123
|
+
if (!token)
|
|
124
|
+
return false;
|
|
125
|
+
token.used = true;
|
|
126
|
+
// Deduct from Supabase wallet balance
|
|
127
|
+
const info = await resolveApiKey();
|
|
128
|
+
if (info) {
|
|
129
|
+
const newBalance = info.walletBalance - token.amount;
|
|
130
|
+
await supabase
|
|
131
|
+
.from("wallets")
|
|
132
|
+
.update({ balance: newBalance })
|
|
133
|
+
.eq("user_id", info.userId);
|
|
134
|
+
// Log the transaction
|
|
135
|
+
await supabase.from("transactions").insert({
|
|
136
|
+
card_id: info.cardId,
|
|
137
|
+
amount: token.amount,
|
|
138
|
+
merchant: token.merchant,
|
|
139
|
+
status: "SUCCESS",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
setTimeout(() => tokenStore.delete(tokenId), 5000);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface VirtualCard {
|
|
2
|
+
alias: string;
|
|
3
|
+
number: string;
|
|
4
|
+
exp_month: string;
|
|
5
|
+
exp_year: string;
|
|
6
|
+
cvv: string;
|
|
7
|
+
name: string;
|
|
8
|
+
balance: number;
|
|
9
|
+
currency: string;
|
|
10
|
+
}
|
|
11
|
+
export interface PaymentToken {
|
|
12
|
+
token: string;
|
|
13
|
+
card_alias: string;
|
|
14
|
+
amount: number;
|
|
15
|
+
merchant: string;
|
|
16
|
+
created_at: number;
|
|
17
|
+
ttl_seconds: number;
|
|
18
|
+
used: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface CardData {
|
|
21
|
+
number: string;
|
|
22
|
+
exp_month: string;
|
|
23
|
+
exp_year: string;
|
|
24
|
+
cvv: string;
|
|
25
|
+
name: string;
|
|
26
|
+
}
|
|
27
|
+
export interface PaymentResult {
|
|
28
|
+
success: boolean;
|
|
29
|
+
message: string;
|
|
30
|
+
receipt_id?: string;
|
|
31
|
+
amount?: number;
|
|
32
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "z-zero-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Z-ZERO MCP Server — Secure JIT Virtual Card Payment tools for AI Agents (Claude, AutoGPT, CrewAI)",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"z-zero-mcp-server": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"start": "node dist/index.js",
|
|
12
|
+
"dev": "npx tsx src/index.ts",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"model-context-protocol",
|
|
18
|
+
"ai-agent",
|
|
19
|
+
"virtual-card",
|
|
20
|
+
"payment",
|
|
21
|
+
"z-zero",
|
|
22
|
+
"clawcard",
|
|
23
|
+
"jit",
|
|
24
|
+
"fintech"
|
|
25
|
+
],
|
|
26
|
+
"author": "Z-ZERO",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"files": [
|
|
29
|
+
"dist/",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
34
|
+
"@supabase/supabase-js": "^2.49.4",
|
|
35
|
+
"dotenv": "^16.5.0",
|
|
36
|
+
"playwright": "^1.52.0",
|
|
37
|
+
"zod": "^3.25.11"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"typescript": "^5.8.3",
|
|
41
|
+
"tsx": "^4.19.4",
|
|
42
|
+
"@types/node": "^22.15.21"
|
|
43
|
+
}
|
|
44
|
+
}
|