@bloque/cli 0.0.45

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.
Files changed (3) hide show
  1. package/README.md +152 -0
  2. package/dist/bin.js +2371 -0
  3. package/package.json +49 -0
package/dist/bin.js ADDED
@@ -0,0 +1,2371 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/bin.ts
4
+ import { Command as Command6 } from "commander";
5
+
6
+ // src/commands/login.ts
7
+ import { Command } from "commander";
8
+ import { SDK } from "@bloque/sdk";
9
+ import { input, password, select } from "@inquirer/prompts";
10
+
11
+ // src/session/store.ts
12
+ import fs from "fs";
13
+ import os from "os";
14
+ import path from "path";
15
+ var SessionStore = class _SessionStore {
16
+ static SESSION_DIR = path.join(os.homedir(), ".bloque");
17
+ static SESSION_PATH = path.join(_SessionStore.SESSION_DIR, "session.json");
18
+ save(session) {
19
+ fs.mkdirSync(_SessionStore.SESSION_DIR, { recursive: true, mode: 448 });
20
+ fs.writeFileSync(
21
+ _SessionStore.SESSION_PATH,
22
+ JSON.stringify(session, null, 2),
23
+ { mode: 384 }
24
+ );
25
+ }
26
+ load() {
27
+ try {
28
+ const data = fs.readFileSync(_SessionStore.SESSION_PATH, "utf-8");
29
+ return JSON.parse(data);
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+ clear() {
35
+ try {
36
+ fs.unlinkSync(_SessionStore.SESSION_PATH);
37
+ } catch {
38
+ }
39
+ }
40
+ exists() {
41
+ return fs.existsSync(_SessionStore.SESSION_PATH);
42
+ }
43
+ };
44
+
45
+ // src/ui/portal.ts
46
+ import { stdout } from "process";
47
+ var ESC = "\x1B";
48
+ var RESET = `${ESC}[0m`;
49
+ var BOLD = `${ESC}[1m`;
50
+ var HIDE_CURSOR = `${ESC}[?25l`;
51
+ var SHOW_CURSOR = `${ESC}[?25h`;
52
+ var PALETTE = [
53
+ `${ESC}[38;5;53m`,
54
+ // 0 — deep magenta
55
+ `${ESC}[38;5;54m`,
56
+ // 1 — dark purple
57
+ `${ESC}[38;5;55m`,
58
+ // 2 — plum
59
+ `${ESC}[38;5;92m`,
60
+ // 3 — muted violet
61
+ `${ESC}[38;5;93m`,
62
+ // 4 — violet
63
+ `${ESC}[38;5;129m`,
64
+ // 5 — purple
65
+ `${ESC}[38;5;135m`,
66
+ // 6 — light purple
67
+ `${ESC}[38;5;141m`,
68
+ // 7 — lavender
69
+ `${ESC}[38;5;111m`,
70
+ // 8 — soft blue
71
+ `${ESC}[38;5;75m`,
72
+ // 9 — sky blue
73
+ `${ESC}[38;5;81m`,
74
+ // 10 — cyan
75
+ `${ESC}[38;5;123m`,
76
+ // 11 — bright cyan
77
+ `${ESC}[38;5;159m`,
78
+ // 12 — ice blue
79
+ `${ESC}[38;5;195m`,
80
+ // 13 — pale cyan
81
+ `${ESC}[38;5;231m`
82
+ // 14 — white
83
+ ];
84
+ var SUCCESS_COLOR = `${ESC}[38;5;49m`;
85
+ var DIM_STAR = `${ESC}[38;5;236m`;
86
+ var MED_STAR = `${ESC}[38;5;240m`;
87
+ var MAX_RADIUS = 6;
88
+ var WIDTH = 52;
89
+ var HEIGHT = MAX_RADIUS * 2 + 3;
90
+ var CX = Math.floor(WIDTH / 2);
91
+ var CY = Math.floor(HEIGHT / 2);
92
+ var ASPECT = 2.1;
93
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
94
+ function generateStarField() {
95
+ const field = [];
96
+ for (let y = 0; y < HEIGHT; y++) {
97
+ field[y] = [];
98
+ for (let x = 0; x < WIDTH; x++) {
99
+ field[y][x] = Math.random() < 0.025;
100
+ }
101
+ }
102
+ return field;
103
+ }
104
+ function renderFrame(radius, stars, shimmer = false) {
105
+ const lines = [];
106
+ for (let y = 0; y < HEIGHT; y++) {
107
+ let line = "";
108
+ for (let x = 0; x < WIDTH; x++) {
109
+ const dx = (x - CX) / ASPECT;
110
+ const dy = y - CY;
111
+ const dist = Math.sqrt(dx * dx + dy * dy);
112
+ let char = " ";
113
+ let color = "";
114
+ for (let r = 0; r <= radius; r++) {
115
+ const hitThreshold = r === 0 ? 0.5 : 0.6;
116
+ if (Math.abs(dist - r) < hitThreshold) {
117
+ const t = 1 - r / MAX_RADIUS;
118
+ const idx = Math.round(t * (PALETTE.length - 1));
119
+ color = PALETTE[idx];
120
+ if (r === 0) {
121
+ char = "\u2726";
122
+ color = BOLD + PALETTE[PALETTE.length - 1];
123
+ } else if (r <= 2) {
124
+ char = shimmer && Math.random() < 0.3 ? "\u2727" : "\u25E6";
125
+ } else {
126
+ char = shimmer && Math.random() < 0.15 ? "\u2727" : "\xB7";
127
+ }
128
+ break;
129
+ }
130
+ }
131
+ if (char === " " && stars[y][x]) {
132
+ const twinkle = shimmer && Math.random() < 0.3;
133
+ color = twinkle ? MED_STAR : DIM_STAR;
134
+ char = ".";
135
+ }
136
+ line += char !== " " ? color + char + RESET : " ";
137
+ }
138
+ lines.push(line);
139
+ }
140
+ return lines;
141
+ }
142
+ function renderSuccess(message) {
143
+ const pad = " ".repeat(Math.max(0, CX - 1));
144
+ return [
145
+ "",
146
+ `${pad}${BOLD}${SUCCESS_COLOR}\u25C6${RESET} ${SUCCESS_COLOR}${message}${RESET}`,
147
+ ""
148
+ ];
149
+ }
150
+ function writeLines(lines) {
151
+ stdout.write(lines.join("\n") + "\n");
152
+ }
153
+ function clearLines(count) {
154
+ stdout.write(`${ESC}[${count}A${ESC}[0J`);
155
+ }
156
+ async function portalAnimation(message) {
157
+ if (!stdout.isTTY) {
158
+ console.log(`
159
+ \u25C6 ${message}
160
+ `);
161
+ return;
162
+ }
163
+ stdout.write(HIDE_CURSOR);
164
+ const stars = generateStarField();
165
+ try {
166
+ const frame0 = renderFrame(0, stars);
167
+ writeLines(frame0);
168
+ await sleep(120);
169
+ for (let r = 1; r <= MAX_RADIUS; r++) {
170
+ clearLines(frame0.length);
171
+ const frame = renderFrame(r, stars);
172
+ writeLines(frame);
173
+ await sleep(70 + r * 10);
174
+ }
175
+ for (let i = 0; i < 4; i++) {
176
+ clearLines(HEIGHT);
177
+ const frame = renderFrame(MAX_RADIUS, stars, true);
178
+ writeLines(frame);
179
+ await sleep(100);
180
+ }
181
+ for (let r = MAX_RADIUS; r >= 1; r--) {
182
+ clearLines(HEIGHT);
183
+ const frame = renderFrame(r, stars);
184
+ writeLines(frame);
185
+ await sleep(50 + r * 5);
186
+ }
187
+ clearLines(HEIGHT);
188
+ const dotFrame = renderFrame(0, stars);
189
+ writeLines(dotFrame);
190
+ await sleep(150);
191
+ clearLines(HEIGHT);
192
+ const successLines = renderSuccess(message);
193
+ writeLines(successLines);
194
+ } finally {
195
+ stdout.write(SHOW_CURSOR);
196
+ }
197
+ }
198
+
199
+ // src/commands/login.ts
200
+ var store = new SessionStore();
201
+ var OTP_CHANNELS = [
202
+ {
203
+ name: "WhatsApp",
204
+ value: "bloque-whatsapp",
205
+ prompt: "Phone number (e.g. +573001234567):",
206
+ sentMessage: "OTP sent via WhatsApp \u2014 check your phone."
207
+ },
208
+ {
209
+ name: "Email",
210
+ value: "bloque-email",
211
+ prompt: "Email address:",
212
+ sentMessage: "OTP sent \u2014 check your inbox."
213
+ }
214
+ ];
215
+ var loginCommand = new Command("login").description("Authenticate with Bloque").option("--api-key <key>", "API key for backend authentication").option("--origin <origin>", "Origin (only needed with --api-key)").option("--alias <alias>", "Alias (required with --api-key)").option("--sandbox", "Use sandbox environment instead of production").action(async (opts) => {
216
+ const { apiKey, origin, alias, sandbox } = opts;
217
+ const mode = sandbox ? "sandbox" : "production";
218
+ if (apiKey) {
219
+ if (!alias) {
220
+ console.error("Error: --alias is required when using --api-key");
221
+ process.exit(1);
222
+ }
223
+ if (!origin) {
224
+ console.error("Error: --origin is required when using --api-key");
225
+ process.exit(1);
226
+ }
227
+ const sdk2 = new SDK({ auth: { type: "apiKey", apiKey }, mode, origin });
228
+ const clients2 = await sdk2.connect(alias);
229
+ store.save({
230
+ accessToken: clients2.accessToken,
231
+ urn: clients2.urn ?? "",
232
+ origin,
233
+ mode,
234
+ authType: "apiKey",
235
+ apiKey,
236
+ alias,
237
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
238
+ });
239
+ await portalAnimation(`Connected as ${clients2.urn ?? alias}`);
240
+ return;
241
+ }
242
+ const channel = await select({
243
+ message: "How would you like to receive your OTP?",
244
+ choices: OTP_CHANNELS.map((c) => ({ name: c.name, value: c.value }))
245
+ });
246
+ const selectedChannel = OTP_CHANNELS.find((c) => c.value === channel);
247
+ const userAlias = await input({ message: selectedChannel.prompt });
248
+ let _token = null;
249
+ const sdk = new SDK({
250
+ auth: { type: "jwt" },
251
+ mode,
252
+ origin: channel,
253
+ tokenStorage: {
254
+ get: () => _token,
255
+ set: (t) => {
256
+ _token = t;
257
+ },
258
+ clear: () => {
259
+ _token = null;
260
+ }
261
+ }
262
+ });
263
+ await sdk.assert(channel, userAlias);
264
+ console.log(selectedChannel.sentMessage);
265
+ const code = await password({ message: "Enter OTP code:", mask: "*" });
266
+ const clients = await sdk.connect(channel, userAlias, code);
267
+ store.save({
268
+ accessToken: clients.accessToken,
269
+ urn: clients.urn ?? "",
270
+ origin: channel,
271
+ mode,
272
+ authType: "jwt",
273
+ alias: userAlias,
274
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
275
+ });
276
+ await portalAnimation(`Connected as ${clients.urn ?? userAlias}`);
277
+ });
278
+
279
+ // src/commands/logout.ts
280
+ import { Command as Command2 } from "commander";
281
+ var store2 = new SessionStore();
282
+ var logoutCommand = new Command2("logout").description("Clear local Bloque session").action(() => {
283
+ store2.clear();
284
+ console.log("Logged out.");
285
+ });
286
+
287
+ // src/commands/whoami.ts
288
+ import { Command as Command3 } from "commander";
289
+ var store3 = new SessionStore();
290
+ var whoamiCommand = new Command3("whoami").description("Show current session info").action(() => {
291
+ const session = store3.load();
292
+ if (!session) {
293
+ console.error("No active session. Run `bloque login` first.");
294
+ process.exit(1);
295
+ }
296
+ console.log(`URN: ${session.urn}`);
297
+ console.log(`Origin: ${session.origin}`);
298
+ console.log(`Mode: ${session.mode}`);
299
+ console.log(`Auth type: ${session.authType}`);
300
+ console.log(`Created: ${session.createdAt}`);
301
+ });
302
+
303
+ // src/commands/mcp.ts
304
+ import { Command as Command4 } from "commander";
305
+ import { SDK as SDK2 } from "@bloque/sdk";
306
+
307
+ // src/mcp/server.ts
308
+ import { randomUUID } from "crypto";
309
+ import { createServer } from "http";
310
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
311
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
312
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
313
+
314
+ // src/mcp/tools/primitives/accounts.ts
315
+ import { z } from "zod/v4";
316
+
317
+ // src/mcp/currency.ts
318
+ var ASSET_MAP = {
319
+ USD: { asset: "DUSD/6", decimals: 6 },
320
+ COP: { asset: "COP/2", decimals: 2 }
321
+ };
322
+ var REVERSE_ASSET_MAP = {
323
+ "DUSD/6": { currency: "USD", decimals: 6 },
324
+ "COP/2": { currency: "COP", decimals: 2 },
325
+ "COPB/6": { currency: "COP", decimals: 6 },
326
+ "KSM/12": { currency: "KSM", decimals: 12 }
327
+ };
328
+ function toRaw(humanAmount, currency) {
329
+ const mapping = ASSET_MAP[currency.toUpperCase()];
330
+ if (!mapping) {
331
+ return { amount: humanAmount, asset: currency };
332
+ }
333
+ const parts = humanAmount.split(".");
334
+ const integerPart = parts[0] ?? "0";
335
+ const fractionalPart = (parts[1] ?? "").padEnd(mapping.decimals, "0").slice(0, mapping.decimals);
336
+ const raw = BigInt(integerPart) * BigInt(10 ** mapping.decimals) + BigInt(fractionalPart);
337
+ return { amount: raw.toString(), asset: mapping.asset };
338
+ }
339
+ function toHuman(rawAmount, asset) {
340
+ const mapping = REVERSE_ASSET_MAP[asset];
341
+ if (!mapping) {
342
+ return { amount: rawAmount, currency: asset };
343
+ }
344
+ const raw = BigInt(rawAmount);
345
+ const divisor = BigInt(10 ** mapping.decimals);
346
+ const integerPart = raw / divisor;
347
+ const fractionalPart = raw % divisor;
348
+ const fracStr = fractionalPart.toString().padStart(mapping.decimals, "0").replace(/0+$/, "");
349
+ const humanAmount = fracStr ? `${integerPart}.${fracStr}` : integerPart.toString();
350
+ return { amount: humanAmount, currency: mapping.currency };
351
+ }
352
+ function humanizeBalance(balance) {
353
+ return Object.entries(balance).map(([asset, values]) => {
354
+ const { currency } = toHuman("0", asset);
355
+ return {
356
+ currency,
357
+ current: toHuman(values.current, asset).amount,
358
+ pending: toHuman(values.pending, asset).amount,
359
+ ...values.in != null ? { in: toHuman(values.in, asset).amount } : {},
360
+ ...values.out != null ? { out: toHuman(values.out, asset).amount } : {}
361
+ };
362
+ });
363
+ }
364
+ function resolveAsset(currency) {
365
+ return ASSET_MAP[currency.toUpperCase()]?.asset ?? currency;
366
+ }
367
+
368
+ // src/mcp/tools/primitives/accounts.ts
369
+ function registerAccountTools(server, clients) {
370
+ server.registerTool(
371
+ "list_accounts",
372
+ {
373
+ description: "List all financial accounts (cards, virtual pockets, polygon, US bank, Bancolombia) owned by the authenticated user. Optionally filter by account type. Use this to discover what accounts exist before operating on them. Returns URN, status, medium type, and balances for each account.",
374
+ inputSchema: {
375
+ medium: z.enum(["card", "virtual", "polygon", "us-account", "bancolombia"]).optional()
376
+ }
377
+ },
378
+ async ({ medium }) => {
379
+ const result = await clients.accounts.list({ medium });
380
+ const accounts = result.accounts.map((account) => ({
381
+ ...account,
382
+ balance: account.balance ? humanizeBalance(account.balance) : void 0
383
+ }));
384
+ return {
385
+ content: [{ type: "text", text: JSON.stringify(accounts, null, 2) }]
386
+ };
387
+ }
388
+ );
389
+ server.registerTool(
390
+ "get_account",
391
+ {
392
+ description: "Get full details of a specific account by its URN. Returns medium-specific fields (e.g. lastFour and detailsUrl for cards, address for polygon, routing/account numbers for US). Use when you already have a URN and need its current state.",
393
+ inputSchema: { urn: z.string() }
394
+ },
395
+ async ({ urn }) => {
396
+ const account = await clients.accounts.get(urn);
397
+ return {
398
+ content: [{ type: "text", text: JSON.stringify(account, null, 2) }]
399
+ };
400
+ }
401
+ );
402
+ server.registerTool(
403
+ "get_balance",
404
+ {
405
+ description: "Get the current balance of a single account by URN. Returns balances in human-readable amounts (USD, COP). Use this to check available funds before a transfer.",
406
+ inputSchema: { urn: z.string() }
407
+ },
408
+ async ({ urn }) => {
409
+ const raw = await clients.accounts.balance(urn);
410
+ const balances = humanizeBalance(raw);
411
+ return {
412
+ content: [{ type: "text", text: JSON.stringify(balances, null, 2) }]
413
+ };
414
+ }
415
+ );
416
+ server.registerTool(
417
+ "get_all_balances",
418
+ {
419
+ description: "Get aggregated balances across ALL accounts owned by the user, grouped by currency. Quick way to see total holdings."
420
+ },
421
+ async () => {
422
+ const raw = await clients.accounts.balances();
423
+ const balances = humanizeBalance(raw);
424
+ return {
425
+ content: [{ type: "text", text: JSON.stringify(balances, null, 2) }]
426
+ };
427
+ }
428
+ );
429
+ }
430
+
431
+ // src/mcp/tools/primitives/virtual.ts
432
+ import { z as z2 } from "zod/v4";
433
+ function registerVirtualTools(server, clients) {
434
+ server.registerTool(
435
+ "create_virtual_account",
436
+ {
437
+ description: "Create a virtual account (also called a 'pocket' or 'ledger'). Virtual accounts hold balances and can be linked to cards and polygon accounts via ledgerId. For creating a card, prefer the 'create_card' workflow which handles the pocket automatically.",
438
+ inputSchema: {
439
+ name: z2.string().optional(),
440
+ ledgerId: z2.string().optional(),
441
+ metadata: z2.record(z2.string()).optional()
442
+ }
443
+ },
444
+ async ({ name, ledgerId, metadata }) => {
445
+ const account = await clients.accounts.virtual.create({
446
+ name,
447
+ ledgerId,
448
+ metadata
449
+ });
450
+ return {
451
+ content: [{ type: "text", text: JSON.stringify(account, null, 2) }]
452
+ };
453
+ }
454
+ );
455
+ server.registerTool(
456
+ "list_virtual_accounts",
457
+ {
458
+ description: "List all virtual accounts (pockets) owned by the user. Shows name, URN, ledgerId, status, and balances."
459
+ },
460
+ async () => {
461
+ const result = await clients.accounts.virtual.list();
462
+ const accounts = result.accounts.map((account) => ({
463
+ ...account,
464
+ balance: account.balance ? humanizeBalance(account.balance) : void 0
465
+ }));
466
+ return {
467
+ content: [{ type: "text", text: JSON.stringify(accounts, null, 2) }]
468
+ };
469
+ }
470
+ );
471
+ }
472
+
473
+ // src/mcp/tools/primitives/card.ts
474
+ import { z as z3 } from "zod/v4";
475
+ function registerCardTools(server, clients) {
476
+ server.registerTool(
477
+ "create_raw_card",
478
+ {
479
+ description: "Low-level: create a card linked to an existing ledgerId. Does NOT create the virtual account or polygon account. For most cases, use the high-level 'create_card' workflow instead.",
480
+ inputSchema: {
481
+ ledgerId: z3.string(),
482
+ name: z3.string().optional(),
483
+ metadata: z3.record(z3.unknown()).optional(),
484
+ webhookUrl: z3.string().optional()
485
+ }
486
+ },
487
+ async ({ ledgerId, name, metadata, webhookUrl }) => {
488
+ const card = await clients.accounts.card.create({
489
+ ledgerId,
490
+ name,
491
+ metadata,
492
+ webhookUrl
493
+ });
494
+ return {
495
+ content: [{ type: "text", text: JSON.stringify(card, null, 2) }]
496
+ };
497
+ }
498
+ );
499
+ server.registerTool(
500
+ "list_cards",
501
+ {
502
+ description: "List all cards owned by the user. Returns card URN, last four digits, status, card type, detailsUrl, ledgerId, and balances."
503
+ },
504
+ async () => {
505
+ const result = await clients.accounts.card.list();
506
+ const cards = result.accounts.map((card) => ({
507
+ ...card,
508
+ balance: card.balance ? humanizeBalance(card.balance) : void 0
509
+ }));
510
+ return {
511
+ content: [{ type: "text", text: JSON.stringify(cards, null, 2) }]
512
+ };
513
+ }
514
+ );
515
+ server.registerTool(
516
+ "freeze_card",
517
+ {
518
+ description: "Temporarily freeze a card, blocking all transactions. The card can be reactivated later with activate_card.",
519
+ inputSchema: { urn: z3.string() }
520
+ },
521
+ async ({ urn }) => {
522
+ const card = await clients.accounts.card.freeze(urn);
523
+ return {
524
+ content: [{ type: "text", text: JSON.stringify(card, null, 2) }]
525
+ };
526
+ }
527
+ );
528
+ server.registerTool(
529
+ "activate_card",
530
+ {
531
+ description: "Activate a card. Use to unfreeze a previously frozen card, or to activate a newly created card.",
532
+ inputSchema: { urn: z3.string() }
533
+ },
534
+ async ({ urn }) => {
535
+ const card = await clients.accounts.card.activate(urn);
536
+ return {
537
+ content: [{ type: "text", text: JSON.stringify(card, null, 2) }]
538
+ };
539
+ }
540
+ );
541
+ server.registerTool(
542
+ "disable_card",
543
+ {
544
+ description: "Permanently disable a card. This is IRREVERSIBLE \u2014 the card cannot be reactivated.",
545
+ inputSchema: { urn: z3.string() }
546
+ },
547
+ async ({ urn }) => {
548
+ const card = await clients.accounts.card.disable(urn);
549
+ return {
550
+ content: [{ type: "text", text: JSON.stringify(card, null, 2) }]
551
+ };
552
+ }
553
+ );
554
+ server.registerTool(
555
+ "update_card_metadata",
556
+ {
557
+ description: "Update the metadata on a card. Metadata controls spending behavior (spending_control, mcc_whitelist, priority_mcc). For a friendlier interface, use 'configure_spending_rules' workflow.",
558
+ inputSchema: {
559
+ urn: z3.string(),
560
+ metadata: z3.record(z3.unknown())
561
+ }
562
+ },
563
+ async ({ urn, metadata }) => {
564
+ const card = await clients.accounts.card.updateMetadata({ urn, metadata });
565
+ return {
566
+ content: [{ type: "text", text: JSON.stringify(card, null, 2) }]
567
+ };
568
+ }
569
+ );
570
+ server.registerTool(
571
+ "rename_card",
572
+ {
573
+ description: "Change the display name of a card.",
574
+ inputSchema: {
575
+ urn: z3.string(),
576
+ name: z3.string()
577
+ }
578
+ },
579
+ async ({ urn, name }) => {
580
+ const card = await clients.accounts.card.updateName(urn, name);
581
+ return {
582
+ content: [{ type: "text", text: JSON.stringify(card, null, 2) }]
583
+ };
584
+ }
585
+ );
586
+ }
587
+
588
+ // src/mcp/tools/primitives/polygon.ts
589
+ import { z as z4 } from "zod/v4";
590
+ function registerPolygonTools(server, clients) {
591
+ server.registerTool(
592
+ "create_polygon_account",
593
+ {
594
+ description: "Low-level: create a Polygon blockchain account. If ledgerId is provided, shares that ledger's balance. For most cases, use the 'create_card' workflow which creates it automatically.",
595
+ inputSchema: {
596
+ ledgerId: z4.string().optional(),
597
+ name: z4.string().optional(),
598
+ metadata: z4.record(z4.string()).optional()
599
+ }
600
+ },
601
+ async ({ ledgerId, name, metadata }) => {
602
+ const account = await clients.accounts.polygon.create({
603
+ ledgerId,
604
+ name,
605
+ metadata
606
+ });
607
+ return {
608
+ content: [{ type: "text", text: JSON.stringify(account, null, 2) }]
609
+ };
610
+ }
611
+ );
612
+ server.registerTool(
613
+ "list_polygon_accounts",
614
+ {
615
+ description: "List all Polygon blockchain accounts. Shows wallet address, network, URN, ledgerId, and balances."
616
+ },
617
+ async () => {
618
+ const result = await clients.accounts.polygon.list();
619
+ const accounts = result.accounts.map((account) => ({
620
+ ...account,
621
+ balance: account.balance ? humanizeBalance(account.balance) : void 0
622
+ }));
623
+ return {
624
+ content: [{ type: "text", text: JSON.stringify(accounts, null, 2) }]
625
+ };
626
+ }
627
+ );
628
+ }
629
+
630
+ // src/mcp/tools/primitives/us.ts
631
+ import { z as z5 } from "zod/v4";
632
+ function registerUsTools(server, clients) {
633
+ server.registerTool(
634
+ "get_us_tos_link",
635
+ {
636
+ description: "Get a Terms of Service acceptance URL that the user must visit before creating a US bank account.",
637
+ inputSchema: { redirectUri: z5.string() }
638
+ },
639
+ async ({ redirectUri }) => {
640
+ const result = await clients.accounts.us.getTosLink({ redirectUri });
641
+ return {
642
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
643
+ };
644
+ }
645
+ );
646
+ server.registerTool(
647
+ "create_us_account",
648
+ {
649
+ description: "Create a US bank account (routing + account number). Requires the user to have accepted TOS first. Returns account and routing numbers for ACH transfers.",
650
+ inputSchema: {
651
+ type: z5.enum(["individual", "business"]),
652
+ firstName: z5.string(),
653
+ lastName: z5.string(),
654
+ email: z5.string(),
655
+ phone: z5.string(),
656
+ address: z5.object({
657
+ streetLine1: z5.string(),
658
+ streetLine2: z5.string().optional(),
659
+ city: z5.string(),
660
+ state: z5.string(),
661
+ postalCode: z5.string(),
662
+ country: z5.string()
663
+ }),
664
+ birthDate: z5.string(),
665
+ taxIdentificationNumber: z5.string(),
666
+ govIdCountry: z5.string(),
667
+ govIdImageFront: z5.string(),
668
+ signedAgreementId: z5.string(),
669
+ name: z5.string().optional(),
670
+ ledgerId: z5.string().optional(),
671
+ metadata: z5.record(z5.unknown()).optional()
672
+ }
673
+ },
674
+ async (params) => {
675
+ const account = await clients.accounts.us.create(params);
676
+ return {
677
+ content: [{ type: "text", text: JSON.stringify(account, null, 2) }]
678
+ };
679
+ }
680
+ );
681
+ server.registerTool(
682
+ "list_us_accounts",
683
+ {
684
+ description: "List all US bank accounts. Shows account/routing numbers, status, and balances."
685
+ },
686
+ async () => {
687
+ const result = await clients.accounts.us.list();
688
+ const accounts = result.accounts.map((account) => ({
689
+ ...account,
690
+ balance: account.balance ? humanizeBalance(account.balance) : void 0
691
+ }));
692
+ return {
693
+ content: [{ type: "text", text: JSON.stringify(accounts, null, 2) }]
694
+ };
695
+ }
696
+ );
697
+ }
698
+
699
+ // src/mcp/tools/primitives/transfers.ts
700
+ import { z as z6 } from "zod/v4";
701
+ function registerTransferTools(server, clients) {
702
+ server.registerTool(
703
+ "transfer",
704
+ {
705
+ description: "Transfer funds between two accounts. Specify source/destination URN, amount in human-readable format (e.g. '100' for $100), and currency ('USD' or 'COP'). The transfer is queued asynchronously. Returns a queueId to track.",
706
+ inputSchema: {
707
+ sourceUrn: z6.string(),
708
+ destinationUrn: z6.string(),
709
+ amount: z6.string(),
710
+ currency: z6.string().default("USD"),
711
+ metadata: z6.record(z6.unknown()).optional()
712
+ }
713
+ },
714
+ async ({ sourceUrn, destinationUrn, amount, currency, metadata }) => {
715
+ const { amount: rawAmount, asset } = toRaw(amount, currency);
716
+ const result = await clients.accounts.transfer({
717
+ sourceUrn,
718
+ destinationUrn,
719
+ amount: rawAmount,
720
+ asset,
721
+ metadata
722
+ });
723
+ const humanized = {
724
+ ...result,
725
+ amount: toHuman(rawAmount, asset).amount,
726
+ currency: toHuman(rawAmount, asset).currency
727
+ };
728
+ return {
729
+ content: [{ type: "text", text: JSON.stringify(humanized, null, 2) }]
730
+ };
731
+ }
732
+ );
733
+ server.registerTool(
734
+ "batch_transfer",
735
+ {
736
+ description: "Execute multiple transfers in a single batch. Useful for payroll, distributions. Operations are auto-chunked into groups of 80.",
737
+ inputSchema: {
738
+ reference: z6.string(),
739
+ operations: z6.array(
740
+ z6.object({
741
+ fromUrn: z6.string(),
742
+ toUrn: z6.string(),
743
+ reference: z6.string(),
744
+ amount: z6.string(),
745
+ currency: z6.string().default("USD"),
746
+ metadata: z6.record(z6.unknown()).optional()
747
+ })
748
+ ),
749
+ metadata: z6.record(z6.unknown()).optional(),
750
+ webhookUrl: z6.string().optional()
751
+ }
752
+ },
753
+ async ({ reference, operations, metadata, webhookUrl }) => {
754
+ const mappedOps = operations.map((op) => {
755
+ const { amount, asset } = toRaw(op.amount, op.currency);
756
+ return {
757
+ fromUrn: op.fromUrn,
758
+ toUrn: op.toUrn,
759
+ reference: op.reference,
760
+ amount,
761
+ asset,
762
+ metadata: op.metadata
763
+ };
764
+ });
765
+ const result = await clients.accounts.batchTransfer({
766
+ reference,
767
+ operations: mappedOps,
768
+ metadata,
769
+ webhookUrl
770
+ });
771
+ return {
772
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
773
+ };
774
+ }
775
+ );
776
+ }
777
+
778
+ // src/mcp/tools/primitives/history.ts
779
+ import { z as z7 } from "zod/v4";
780
+ function registerHistoryTools(server, clients) {
781
+ server.registerTool(
782
+ "list_transactions",
783
+ {
784
+ description: "List transaction history across ALL accounts. Supports filtering by currency, date range, direction (in/out), and pagination. Use this for a global view of all money movement.",
785
+ inputSchema: {
786
+ currency: z7.string().optional(),
787
+ limit: z7.number().optional(),
788
+ before: z7.string().optional(),
789
+ after: z7.string().optional(),
790
+ direction: z7.enum(["in", "out"]).optional(),
791
+ next: z7.string().optional()
792
+ }
793
+ },
794
+ async ({ currency, limit, before, after, direction, next }) => {
795
+ const asset = currency ? resolveAsset(currency) : void 0;
796
+ const result = await clients.accounts.transactions({
797
+ asset,
798
+ limit,
799
+ before,
800
+ after,
801
+ direction,
802
+ next
803
+ });
804
+ const humanizedTxs = result.data.map((tx) => {
805
+ const { amount, currency: cur } = toHuman(tx.amount, tx.asset);
806
+ return { ...tx, amount, currency: cur };
807
+ });
808
+ return {
809
+ content: [
810
+ {
811
+ type: "text",
812
+ text: JSON.stringify(
813
+ {
814
+ data: humanizedTxs,
815
+ pageSize: result.pageSize,
816
+ hasMore: result.hasMore,
817
+ next: result.next
818
+ },
819
+ null,
820
+ 2
821
+ )
822
+ }
823
+ ]
824
+ };
825
+ }
826
+ );
827
+ server.registerTool(
828
+ "list_account_movements",
829
+ {
830
+ description: "List transaction history for a SPECIFIC account by URN. Shows deposits, withdrawals, and transfers with human-readable amounts.",
831
+ inputSchema: {
832
+ urn: z7.string(),
833
+ currency: z7.string().optional(),
834
+ limit: z7.number().optional(),
835
+ before: z7.string().optional(),
836
+ after: z7.string().optional(),
837
+ direction: z7.enum(["in", "out"]).optional(),
838
+ pocket: z7.enum(["main", "pending"]).optional(),
839
+ next: z7.string().optional()
840
+ }
841
+ },
842
+ async ({ urn, currency, limit, before, after, direction, pocket, next }) => {
843
+ const result = await clients.accounts.movements({
844
+ urn,
845
+ asset: currency ? resolveAsset(currency) : void 0,
846
+ limit,
847
+ before,
848
+ after,
849
+ direction,
850
+ pocket,
851
+ next
852
+ });
853
+ const humanizedMovements = result.data.map((mv) => {
854
+ const { amount, currency: cur } = toHuman(mv.amount, mv.asset);
855
+ return { ...mv, amount, currency: cur };
856
+ });
857
+ return {
858
+ content: [
859
+ {
860
+ type: "text",
861
+ text: JSON.stringify(
862
+ {
863
+ data: humanizedMovements,
864
+ pageSize: result.pageSize,
865
+ hasMore: result.hasMore,
866
+ next: result.next
867
+ },
868
+ null,
869
+ 2
870
+ )
871
+ }
872
+ ]
873
+ };
874
+ }
875
+ );
876
+ }
877
+
878
+ // src/mcp/tools/primitives/swap.ts
879
+ import { z as z8 } from "zod/v4";
880
+ function registerSwapTools(server, clients) {
881
+ server.registerTool(
882
+ "find_rates",
883
+ {
884
+ description: "Find exchange rates between two assets. Returns rates with fees, limits, and a rateSig needed to create swap orders. Use this before topup_via_pse or cashout_to_bank.",
885
+ inputSchema: {
886
+ fromAsset: z8.string(),
887
+ toAsset: z8.string(),
888
+ fromMediums: z8.array(z8.string()),
889
+ toMediums: z8.array(z8.string()),
890
+ amountSrc: z8.string().optional(),
891
+ amountDst: z8.string().optional()
892
+ }
893
+ },
894
+ async (params) => {
895
+ const result = await clients.swap.findRates(params);
896
+ return {
897
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
898
+ };
899
+ }
900
+ );
901
+ server.registerTool(
902
+ "list_pse_banks",
903
+ {
904
+ description: "List Colombian banks available for PSE payments. Returns bank code and name."
905
+ },
906
+ async () => {
907
+ const result = await clients.swap.pse.banks();
908
+ return {
909
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
910
+ };
911
+ }
912
+ );
913
+ server.registerTool(
914
+ "create_pse_order",
915
+ {
916
+ description: "Low-level: create a PSE swap order to convert COP into DUSD. For a simpler flow, use 'topup_via_pse' workflow.",
917
+ inputSchema: {
918
+ rateSig: z8.string(),
919
+ toMedium: z8.string(),
920
+ amountSrc: z8.string().optional(),
921
+ amountDst: z8.string().optional(),
922
+ depositUrn: z8.string(),
923
+ bankCode: z8.string(),
924
+ userType: z8.union([z8.literal(0), z8.literal(1)]),
925
+ customerEmail: z8.string(),
926
+ userLegalIdType: z8.enum(["CC", "NIT", "CE"]),
927
+ userLegalId: z8.string(),
928
+ fullName: z8.string(),
929
+ phoneNumber: z8.string().optional(),
930
+ webhookUrl: z8.string().optional()
931
+ }
932
+ },
933
+ async ({
934
+ rateSig,
935
+ toMedium,
936
+ amountSrc,
937
+ amountDst,
938
+ depositUrn,
939
+ bankCode,
940
+ userType,
941
+ customerEmail,
942
+ userLegalIdType,
943
+ userLegalId,
944
+ fullName,
945
+ phoneNumber,
946
+ webhookUrl
947
+ }) => {
948
+ const result = await clients.swap.pse.create({
949
+ rateSig,
950
+ toMedium,
951
+ webhookUrl,
952
+ amountSrc,
953
+ amountDst,
954
+ depositInformation: { urn: depositUrn },
955
+ args: {
956
+ bankCode,
957
+ userType,
958
+ customerEmail,
959
+ userLegalIdType,
960
+ userLegalId,
961
+ customerData: { fullName, phoneNumber: phoneNumber ?? "" }
962
+ }
963
+ });
964
+ return {
965
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
966
+ };
967
+ }
968
+ );
969
+ server.registerTool(
970
+ "create_bank_transfer_order",
971
+ {
972
+ description: "Low-level: create a bank transfer order to cash out DUSD to COP. For a simpler flow, use 'cashout_to_bank' workflow.",
973
+ inputSchema: {
974
+ rateSig: z8.string(),
975
+ toMedium: z8.string(),
976
+ amountSrc: z8.string().optional(),
977
+ amountDst: z8.string().optional(),
978
+ sourceAccountUrn: z8.string(),
979
+ bankAccountType: z8.enum(["savings", "checking"]),
980
+ bankAccountNumber: z8.string(),
981
+ bankAccountHolderName: z8.string(),
982
+ bankAccountHolderIdentificationType: z8.enum(["CC", "CE", "NIT", "PP"]),
983
+ bankAccountHolderIdentificationValue: z8.string(),
984
+ webhookUrl: z8.string().optional()
985
+ }
986
+ },
987
+ async ({
988
+ rateSig,
989
+ toMedium,
990
+ amountSrc,
991
+ amountDst,
992
+ sourceAccountUrn,
993
+ bankAccountType,
994
+ bankAccountNumber,
995
+ bankAccountHolderName,
996
+ bankAccountHolderIdentificationType,
997
+ bankAccountHolderIdentificationValue,
998
+ webhookUrl
999
+ }) => {
1000
+ const result = await clients.swap.bankTransfer.create({
1001
+ rateSig,
1002
+ toMedium,
1003
+ webhookUrl,
1004
+ amountSrc,
1005
+ amountDst,
1006
+ depositInformation: {
1007
+ bankAccountType,
1008
+ bankAccountNumber,
1009
+ bankAccountHolderName,
1010
+ bankAccountHolderIdentificationType,
1011
+ bankAccountHolderIdentificationValue
1012
+ },
1013
+ args: { sourceAccountUrn }
1014
+ });
1015
+ return {
1016
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1017
+ };
1018
+ }
1019
+ );
1020
+ }
1021
+
1022
+ // src/mcp/tools/workflows/identity.ts
1023
+ function registerIdentityWorkflows(server, clients) {
1024
+ server.registerTool(
1025
+ "get_profile",
1026
+ {
1027
+ description: "Get the authenticated user's identity profile and verification status. Returns name, email, phone, legal ID, address, and KYC status (approved/awaiting/rejected). Use this as the first call to understand who the user is, check if they can create cards (requires KYC approval), and to auto-fill forms like PSE top-ups. If KYC is not approved, tell the user to run verify_identity first."
1028
+ },
1029
+ async () => {
1030
+ const me = await clients.identity.me();
1031
+ let kyc = {};
1032
+ try {
1033
+ kyc = await clients.compliance.kyc.getVerification({ urn: me.urn });
1034
+ } catch {
1035
+ kyc = { status: "unknown" };
1036
+ }
1037
+ const profile = {
1038
+ urn: me.urn,
1039
+ firstName: me.profile.first_name,
1040
+ lastName: me.profile.last_name,
1041
+ email: me.profile.email,
1042
+ phone: me.profile.phone,
1043
+ legalIdType: me.profile.personal_id_type,
1044
+ legalIdNumber: me.profile.personal_id_number,
1045
+ address: {
1046
+ line1: me.profile.address_line1,
1047
+ line2: me.profile.address_line2,
1048
+ city: me.profile.city,
1049
+ state: me.profile.state,
1050
+ postalCode: me.profile.postal_code,
1051
+ country: me.profile.country_of_residence_code
1052
+ },
1053
+ birthdate: me.profile.birthdate,
1054
+ kycStatus: kyc.status ?? "unknown",
1055
+ canCreateCards: kyc.status === "approved"
1056
+ };
1057
+ return {
1058
+ content: [{ type: "text", text: JSON.stringify(profile, null, 2) }]
1059
+ };
1060
+ }
1061
+ );
1062
+ server.registerTool(
1063
+ "verify_identity",
1064
+ {
1065
+ description: "Start or check identity verification (KYC). If the user has not started verification, initiates it and returns a URL to complete the process. The user MUST complete KYC before they can create cards. Call get_profile to check status afterward."
1066
+ },
1067
+ async () => {
1068
+ const me = await clients.identity.me();
1069
+ let kyc = {};
1070
+ try {
1071
+ kyc = await clients.compliance.kyc.getVerification({ urn: me.urn });
1072
+ } catch {
1073
+ }
1074
+ if (kyc.status === "approved") {
1075
+ return {
1076
+ content: [
1077
+ {
1078
+ type: "text",
1079
+ text: JSON.stringify({ status: "approved", completedAt: kyc.completedAt }, null, 2)
1080
+ }
1081
+ ]
1082
+ };
1083
+ }
1084
+ let result = kyc;
1085
+ try {
1086
+ result = await clients.compliance.kyc.startVerification({ urn: me.urn });
1087
+ } catch {
1088
+ }
1089
+ return {
1090
+ content: [
1091
+ {
1092
+ type: "text",
1093
+ text: JSON.stringify(
1094
+ { status: result.status, verificationUrl: result.url, completedAt: result.completedAt },
1095
+ null,
1096
+ 2
1097
+ )
1098
+ }
1099
+ ]
1100
+ };
1101
+ }
1102
+ );
1103
+ }
1104
+
1105
+ // src/mcp/tools/workflows/account.ts
1106
+ import { z as z9 } from "zod/v4";
1107
+ function registerAccountWorkflows(server, clients) {
1108
+ server.registerTool(
1109
+ "create_account",
1110
+ {
1111
+ description: "Create a financial account \u2014 the place where money lives. Sets up a virtual pocket (holds the balance) and a Polygon blockchain address (for receiving USDC/crypto) sharing the same ledger. Use this when you need a shared balance that multiple cards will draw from. For a single-card setup, use create_card directly (it creates the account automatically).",
1112
+ inputSchema: {
1113
+ name: z9.string().optional().default("Account"),
1114
+ fundFromUrn: z9.string().optional(),
1115
+ fundAmount: z9.string().optional(),
1116
+ currency: z9.string().optional().default("USD")
1117
+ }
1118
+ },
1119
+ async ({ name, fundFromUrn, fundAmount, currency }) => {
1120
+ const pocket = await clients.accounts.virtual.create({ name });
1121
+ const polygon = await clients.accounts.polygon.create({ ledgerId: pocket.ledgerId, name });
1122
+ let transferResult;
1123
+ if (fundFromUrn && fundAmount) {
1124
+ const { amount: rawAmount, asset } = toRaw(fundAmount, currency);
1125
+ transferResult = await clients.accounts.transfer({
1126
+ sourceUrn: fundFromUrn,
1127
+ destinationUrn: pocket.urn,
1128
+ amount: rawAmount,
1129
+ asset
1130
+ });
1131
+ }
1132
+ const result = {
1133
+ account: { urn: pocket.urn, ledgerId: pocket.ledgerId },
1134
+ polygon: { urn: polygon.urn, address: polygon.address, network: polygon.network },
1135
+ funded: !!transferResult,
1136
+ transferResult
1137
+ };
1138
+ return {
1139
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1140
+ };
1141
+ }
1142
+ );
1143
+ }
1144
+
1145
+ // src/mcp/tools/workflows/card.ts
1146
+ import { z as z10 } from "zod/v4";
1147
+
1148
+ // src/mcp/categories.ts
1149
+ var CATEGORY_PRESETS = {
1150
+ food: ["5411", "5412", "5441", "5451", "5462", "5499", "5812", "5813", "5814"],
1151
+ transport: ["4111", "4121", "4131", "4784", "5541", "5542", "7512"],
1152
+ entertainment: ["7832", "7841", "7911", "7922", "7929", "7932", "7933", "7941"],
1153
+ health: ["5912", "8011", "8021", "8031", "8041", "8042", "8043", "8049", "8050", "8062"],
1154
+ shopping: ["5311", "5331", "5399", "5611", "5621", "5631", "5641", "5651", "5661", "5691", "5699"],
1155
+ ads: ["7311", "7312", "7372", "7375", "5734"],
1156
+ travel: [
1157
+ "3000",
1158
+ "3001",
1159
+ "3002",
1160
+ "3003",
1161
+ "3004",
1162
+ "3005",
1163
+ "3006",
1164
+ "3007",
1165
+ "3008",
1166
+ "3009",
1167
+ "3010",
1168
+ "4411",
1169
+ "4511",
1170
+ "4722",
1171
+ "7011",
1172
+ "7012",
1173
+ "7032",
1174
+ "7033"
1175
+ ],
1176
+ subscriptions: ["5815", "5816", "5817", "5818", "4899"]
1177
+ };
1178
+ function resolveMccs(categories, rawMccs) {
1179
+ const mccs = /* @__PURE__ */ new Set();
1180
+ if (categories) {
1181
+ for (const cat of categories) {
1182
+ const preset = CATEGORY_PRESETS[cat.toLowerCase()];
1183
+ if (preset) {
1184
+ for (const code of preset) mccs.add(code);
1185
+ }
1186
+ }
1187
+ }
1188
+ if (rawMccs) {
1189
+ for (const code of rawMccs) mccs.add(code);
1190
+ }
1191
+ return [...mccs];
1192
+ }
1193
+
1194
+ // src/mcp/tools/workflows/card.ts
1195
+ function extractDomain(input3) {
1196
+ const trimmed = input3.trim();
1197
+ if (!trimmed) return null;
1198
+ let hostname;
1199
+ try {
1200
+ const withProto = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
1201
+ hostname = new URL(withProto).hostname;
1202
+ } catch {
1203
+ hostname = trimmed.split("/")[0]?.split("?")[0]?.split("#")[0] ?? trimmed;
1204
+ }
1205
+ hostname = hostname.replace(/:\d+$/, "");
1206
+ if (hostname.startsWith("www.")) hostname = hostname.slice(4);
1207
+ return hostname.toLowerCase() || null;
1208
+ }
1209
+ function matchDomain(domain, allowedWebsites) {
1210
+ if (!Array.isArray(allowedWebsites)) return false;
1211
+ const normalized = domain.toLowerCase().replace(/^www\./, "");
1212
+ return allowedWebsites.some((pattern) => {
1213
+ if (typeof pattern !== "string") return false;
1214
+ const p = pattern.toLowerCase().replace(/^www\./, "");
1215
+ return normalized === p || normalized.endsWith(`.${p}`);
1216
+ });
1217
+ }
1218
+ async function pollUntilActive(clients, urn, timeoutMs = 3e4, intervalMs = 2e3) {
1219
+ const deadline = Date.now() + timeoutMs;
1220
+ while (Date.now() < deadline) {
1221
+ const account = await clients.accounts.get(urn);
1222
+ if (account.status === "active") return account.status;
1223
+ await new Promise((r) => setTimeout(r, intervalMs));
1224
+ }
1225
+ return "pending";
1226
+ }
1227
+ function registerCardWorkflows(server, clients) {
1228
+ server.registerTool(
1229
+ "create_card",
1230
+ {
1231
+ description: "Create a card for spending. If accountUrn is provided, the card is attached to that existing account (sharing its balance). If accountUrn is omitted, a new account (pocket + polygon) is created automatically. Optionally restrict to merchant categories using allowedCategories (e.g. 'food', 'transport', 'ads') or allowedMccs (raw MCC codes). Optionally fund the backing account on creation.",
1232
+ inputSchema: {
1233
+ name: z10.string().optional().default("Card"),
1234
+ accountUrn: z10.string().optional(),
1235
+ allowedCategories: z10.array(z10.string()).optional(),
1236
+ allowedMccs: z10.array(z10.string()).optional(),
1237
+ websites: z10.array(z10.string()).optional().describe('Domains this card should be used for (e.g. "amazon.com", "netflix.com")'),
1238
+ fundFromUrn: z10.string().optional(),
1239
+ fundAmount: z10.string().optional(),
1240
+ currency: z10.string().optional().default("USD"),
1241
+ webhookUrl: z10.string().optional()
1242
+ }
1243
+ },
1244
+ async ({
1245
+ name,
1246
+ accountUrn,
1247
+ allowedCategories,
1248
+ allowedMccs,
1249
+ websites,
1250
+ fundFromUrn,
1251
+ fundAmount,
1252
+ currency,
1253
+ webhookUrl
1254
+ }) => {
1255
+ const me = await clients.identity.me();
1256
+ const kyc = await clients.compliance.kyc.getVerification({ urn: me.urn });
1257
+ if (kyc.status !== "approved") {
1258
+ return {
1259
+ content: [{
1260
+ type: "text",
1261
+ text: "KYC verification is not approved. Please run verify_identity first to complete identity verification before creating cards."
1262
+ }],
1263
+ isError: true
1264
+ };
1265
+ }
1266
+ let ledgerId;
1267
+ let virtualAccount;
1268
+ let polygon;
1269
+ if (accountUrn) {
1270
+ const existing = await clients.accounts.get(accountUrn);
1271
+ ledgerId = existing.ledgerId;
1272
+ virtualAccount = { urn: accountUrn, ledgerId };
1273
+ const polygonList = await clients.accounts.list({ medium: "polygon" });
1274
+ const match = polygonList.accounts.find((a) => a.ledgerId === ledgerId);
1275
+ polygon = match ? { urn: match.urn, address: match.address, network: match.network } : { urn: "", address: "", network: "" };
1276
+ } else {
1277
+ const pocket = await clients.accounts.virtual.create({ name });
1278
+ const poly = await clients.accounts.polygon.create({ ledgerId: pocket.ledgerId, name });
1279
+ ledgerId = pocket.ledgerId;
1280
+ virtualAccount = { urn: pocket.urn, ledgerId: pocket.ledgerId };
1281
+ polygon = { urn: poly.urn, address: poly.address, network: poly.network };
1282
+ }
1283
+ const card = await clients.accounts.card.create(
1284
+ { ledgerId, name, webhookUrl },
1285
+ { waitLedger: true }
1286
+ );
1287
+ await pollUntilActive(clients, card.urn);
1288
+ const mccs = resolveMccs(allowedCategories, allowedMccs);
1289
+ const normalizedWebsites = (websites ?? []).map((w) => extractDomain(w)).filter((d) => d !== null);
1290
+ if (mccs.length > 0 || normalizedWebsites.length > 0) {
1291
+ const metadata = {};
1292
+ if (mccs.length > 0) {
1293
+ metadata.spending_control = "default";
1294
+ metadata.mcc_whitelist = mccs;
1295
+ metadata.preferred_asset = "DUSD/6";
1296
+ metadata.default_asset = "DUSD/6";
1297
+ }
1298
+ if (normalizedWebsites.length > 0) {
1299
+ metadata.allowed_websites = normalizedWebsites;
1300
+ }
1301
+ await clients.accounts.card.updateMetadata({
1302
+ urn: card.urn,
1303
+ metadata
1304
+ });
1305
+ }
1306
+ let transferResult;
1307
+ if (fundFromUrn && fundAmount) {
1308
+ const { amount: rawAmount, asset } = toRaw(fundAmount, currency);
1309
+ transferResult = await clients.accounts.transfer({
1310
+ sourceUrn: fundFromUrn,
1311
+ destinationUrn: virtualAccount.urn,
1312
+ amount: rawAmount,
1313
+ asset
1314
+ });
1315
+ }
1316
+ const result = {
1317
+ card: {
1318
+ urn: card.urn,
1319
+ lastFour: card.lastFour,
1320
+ status: card.status,
1321
+ detailsUrl: card.detailsUrl,
1322
+ ledgerId: card.ledgerId
1323
+ },
1324
+ account: { urn: virtualAccount.urn, ledgerId },
1325
+ polygon,
1326
+ funded: !!transferResult,
1327
+ transferResult
1328
+ };
1329
+ return {
1330
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1331
+ };
1332
+ }
1333
+ );
1334
+ server.registerTool(
1335
+ "create_disposable_card",
1336
+ {
1337
+ description: "Create a one-time disposable card with an exact funded amount. Creates an isolated account + card, funds it with the exact amount, so the card can never be charged more. Perfect for untrusted online purchases. Call disable_card after use.",
1338
+ inputSchema: {
1339
+ name: z10.string().optional().default("Disposable Card"),
1340
+ sourceUrn: z10.string(),
1341
+ amount: z10.string(),
1342
+ currency: z10.string().optional().default("USD"),
1343
+ allowedCategories: z10.array(z10.string()).optional(),
1344
+ allowedMccs: z10.array(z10.string()).optional(),
1345
+ websites: z10.array(z10.string()).optional().describe('Domains this card should be used for (e.g. "amazon.com")'),
1346
+ webhookUrl: z10.string().optional()
1347
+ }
1348
+ },
1349
+ async ({
1350
+ name,
1351
+ sourceUrn,
1352
+ amount,
1353
+ currency,
1354
+ allowedCategories,
1355
+ allowedMccs,
1356
+ websites,
1357
+ webhookUrl
1358
+ }) => {
1359
+ const me = await clients.identity.me();
1360
+ const kyc = await clients.compliance.kyc.getVerification({ urn: me.urn });
1361
+ if (kyc.status !== "approved") {
1362
+ return {
1363
+ content: [{
1364
+ type: "text",
1365
+ text: "KYC verification is not approved. Please run verify_identity first to complete identity verification before creating cards."
1366
+ }],
1367
+ isError: true
1368
+ };
1369
+ }
1370
+ const pocket = await clients.accounts.virtual.create({ name });
1371
+ const polygon = await clients.accounts.polygon.create({
1372
+ ledgerId: pocket.ledgerId,
1373
+ name
1374
+ });
1375
+ const card = await clients.accounts.card.create(
1376
+ { ledgerId: pocket.ledgerId, name, webhookUrl },
1377
+ { waitLedger: true }
1378
+ );
1379
+ await pollUntilActive(clients, card.urn);
1380
+ const { amount: rawAmount, asset } = toRaw(amount, currency);
1381
+ const transferResult = await clients.accounts.transfer({
1382
+ sourceUrn,
1383
+ destinationUrn: pocket.urn,
1384
+ amount: rawAmount,
1385
+ asset
1386
+ });
1387
+ const mccs = resolveMccs(allowedCategories, allowedMccs);
1388
+ const normalizedWebsites = (websites ?? []).map((w) => extractDomain(w)).filter((d) => d !== null);
1389
+ if (mccs.length > 0 || normalizedWebsites.length > 0) {
1390
+ const metadata = {};
1391
+ if (mccs.length > 0) {
1392
+ metadata.spending_control = "default";
1393
+ metadata.mcc_whitelist = mccs;
1394
+ metadata.preferred_asset = "DUSD/6";
1395
+ metadata.default_asset = "DUSD/6";
1396
+ }
1397
+ if (normalizedWebsites.length > 0) {
1398
+ metadata.allowed_websites = normalizedWebsites;
1399
+ }
1400
+ await clients.accounts.card.updateMetadata({
1401
+ urn: card.urn,
1402
+ metadata
1403
+ });
1404
+ }
1405
+ const result = {
1406
+ card: {
1407
+ urn: card.urn,
1408
+ lastFour: card.lastFour,
1409
+ status: card.status,
1410
+ detailsUrl: card.detailsUrl,
1411
+ ledgerId: card.ledgerId
1412
+ },
1413
+ account: { urn: pocket.urn, ledgerId: pocket.ledgerId },
1414
+ polygon: { urn: polygon.urn, address: polygon.address, network: polygon.network },
1415
+ funded: true,
1416
+ transferResult
1417
+ };
1418
+ return {
1419
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1420
+ };
1421
+ }
1422
+ );
1423
+ server.registerTool(
1424
+ "resolve_card_for_website",
1425
+ {
1426
+ description: "Find which card(s) to use for a given website. Searches allowed_websites in card metadata. Returns all active matching cards with balances. If no card matches, returns all active cards so you can pick or assign one.",
1427
+ inputSchema: {
1428
+ url: z10.string().describe('URL or domain name to match (e.g. "https://amazon.com/dp/123" or "amazon.com")')
1429
+ }
1430
+ },
1431
+ async ({ url }) => {
1432
+ const domain = extractDomain(url);
1433
+ if (!domain) {
1434
+ return {
1435
+ content: [{ type: "text", text: "A URL or domain name is required." }],
1436
+ isError: true
1437
+ };
1438
+ }
1439
+ const result = await clients.accounts.card.list();
1440
+ const activeCards = result.accounts.filter(
1441
+ (c) => c.status === "active"
1442
+ );
1443
+ const matches = activeCards.filter(
1444
+ (c) => matchDomain(domain, c.metadata?.allowed_websites)
1445
+ );
1446
+ const formatCard = (card) => ({
1447
+ urn: card.urn,
1448
+ name: card.metadata?.name,
1449
+ lastFour: card.lastFour,
1450
+ status: card.status,
1451
+ websites: Array.isArray(card.metadata?.allowed_websites) ? card.metadata.allowed_websites : [],
1452
+ balance: card.balance ? humanizeBalance(card.balance) : void 0,
1453
+ ledgerId: card.ledgerId
1454
+ });
1455
+ if (matches.length > 0) {
1456
+ return {
1457
+ content: [{
1458
+ type: "text",
1459
+ text: JSON.stringify({
1460
+ searchedDomain: domain,
1461
+ matched: true,
1462
+ cards: matches.map(formatCard)
1463
+ }, null, 2)
1464
+ }]
1465
+ };
1466
+ }
1467
+ return {
1468
+ content: [{
1469
+ type: "text",
1470
+ text: JSON.stringify({
1471
+ searchedDomain: domain,
1472
+ matched: false,
1473
+ message: `No card is assigned to "${domain}". Here are all active cards \u2014 you can assign one using assign_card_to_website.`,
1474
+ cards: activeCards.map(formatCard)
1475
+ }, null, 2)
1476
+ }]
1477
+ };
1478
+ }
1479
+ );
1480
+ server.registerTool(
1481
+ "assign_card_to_website",
1482
+ {
1483
+ description: "Associate a card with one or more websites. The agent will then be able to resolve which card to use for a given website via resolve_card_for_website. By default, websites are merged with any existing website assignments. Set merge=false to replace the list entirely.",
1484
+ inputSchema: {
1485
+ cardUrn: z10.string(),
1486
+ websites: z10.array(z10.string()).describe('Domains to assign (e.g. ["amazon.com", "aws.amazon.com"])'),
1487
+ merge: z10.boolean().optional().default(true).describe("Merge with existing websites (true) or replace them (false)")
1488
+ }
1489
+ },
1490
+ async ({ cardUrn, websites, merge }) => {
1491
+ const normalizedNew = websites.map((w) => extractDomain(w)).filter((d) => d !== null);
1492
+ if (normalizedNew.length === 0) {
1493
+ return {
1494
+ content: [{ type: "text", text: "At least one valid domain is required." }],
1495
+ isError: true
1496
+ };
1497
+ }
1498
+ const card = await clients.accounts.get(cardUrn);
1499
+ const existing = card.metadata ?? {};
1500
+ let mergedWebsites;
1501
+ if (merge && Array.isArray(existing.allowed_websites)) {
1502
+ const validExisting = existing.allowed_websites.filter(
1503
+ (w) => typeof w === "string"
1504
+ );
1505
+ mergedWebsites = [.../* @__PURE__ */ new Set([...validExisting, ...normalizedNew])];
1506
+ } else {
1507
+ mergedWebsites = [...new Set(normalizedNew)];
1508
+ }
1509
+ const updatedCard = await clients.accounts.card.updateMetadata({
1510
+ urn: cardUrn,
1511
+ metadata: {
1512
+ ...existing,
1513
+ allowed_websites: mergedWebsites
1514
+ }
1515
+ });
1516
+ return {
1517
+ content: [{
1518
+ type: "text",
1519
+ text: JSON.stringify({
1520
+ card: {
1521
+ urn: updatedCard.urn,
1522
+ lastFour: updatedCard.lastFour,
1523
+ status: updatedCard.status
1524
+ },
1525
+ allowedWebsites: mergedWebsites
1526
+ }, null, 2)
1527
+ }]
1528
+ };
1529
+ }
1530
+ );
1531
+ }
1532
+
1533
+ // src/mcp/tools/workflows/fund-card.ts
1534
+ import { z as z11 } from "zod/v4";
1535
+ function registerFundCardWorkflows(server, clients) {
1536
+ server.registerTool(
1537
+ "fund_card",
1538
+ {
1539
+ description: "Add funds to a card (tops up its backing account). Provide the card URN and a source \u2014 the tool resolves the card's backing account automatically. If multiple cards share the same account, all see the updated balance.",
1540
+ inputSchema: {
1541
+ cardUrn: z11.string(),
1542
+ sourceUrn: z11.string(),
1543
+ amount: z11.string(),
1544
+ currency: z11.string().optional().default("USD")
1545
+ }
1546
+ },
1547
+ async ({ cardUrn, sourceUrn, amount, currency }) => {
1548
+ const cardAccount = await clients.accounts.get(cardUrn);
1549
+ const { ledgerId } = cardAccount;
1550
+ const { accounts: virtualAccounts } = await clients.accounts.list({ medium: "virtual" });
1551
+ const pocket = virtualAccounts.find((a) => a.ledgerId === ledgerId);
1552
+ if (!pocket) {
1553
+ return {
1554
+ content: [{
1555
+ type: "text",
1556
+ text: JSON.stringify(
1557
+ { error: `No virtual pocket found with ledgerId ${ledgerId} for card ${cardUrn}` },
1558
+ null,
1559
+ 2
1560
+ )
1561
+ }]
1562
+ };
1563
+ }
1564
+ const { amount: rawAmount, asset } = toRaw(amount, currency);
1565
+ const transferResult = await clients.accounts.transfer({
1566
+ sourceUrn,
1567
+ destinationUrn: pocket.urn,
1568
+ amount: rawAmount,
1569
+ asset
1570
+ });
1571
+ const balance = await clients.accounts.balance(pocket.urn);
1572
+ const result = {
1573
+ transferResult,
1574
+ accountBalance: humanizeBalance(balance)
1575
+ };
1576
+ return {
1577
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1578
+ };
1579
+ }
1580
+ );
1581
+ }
1582
+
1583
+ // src/mcp/tools/workflows/topup.ts
1584
+ import { z as z12 } from "zod/v4";
1585
+ function registerTopupWorkflows(server, clients) {
1586
+ server.registerTool(
1587
+ "topup_via_pse",
1588
+ {
1589
+ description: "Top up an account with Colombian pesos (COP) via PSE bank transfer. Finds the best exchange rate, creates the PSE order, and returns a checkout URL for the user to complete payment at their bank.",
1590
+ inputSchema: {
1591
+ accountUrn: z12.string(),
1592
+ amount: z12.string(),
1593
+ bankCode: z12.string(),
1594
+ userType: z12.union([z12.literal(0), z12.literal(1)]),
1595
+ customerEmail: z12.string(),
1596
+ userLegalIdType: z12.enum(["CC", "NIT", "CE"]),
1597
+ userLegalId: z12.string(),
1598
+ fullName: z12.string(),
1599
+ phoneNumber: z12.string().optional(),
1600
+ webhookUrl: z12.string().optional()
1601
+ }
1602
+ },
1603
+ async ({
1604
+ accountUrn,
1605
+ amount,
1606
+ bankCode,
1607
+ userType,
1608
+ customerEmail,
1609
+ userLegalIdType,
1610
+ userLegalId,
1611
+ fullName,
1612
+ phoneNumber,
1613
+ webhookUrl
1614
+ }) => {
1615
+ const { amount: rawAmount } = toRaw(amount, "COP");
1616
+ const ratesResult = await clients.swap.findRates({
1617
+ fromAsset: "COP/2",
1618
+ toAsset: "DUSD/6",
1619
+ fromMediums: ["pse"],
1620
+ toMediums: ["kusama"],
1621
+ amountSrc: rawAmount
1622
+ });
1623
+ const rate = ratesResult.rates[0];
1624
+ if (!rate) {
1625
+ return {
1626
+ content: [{
1627
+ type: "text",
1628
+ text: JSON.stringify({ error: "No exchange rates available for COP \u2192 USD via PSE" }, null, 2)
1629
+ }]
1630
+ };
1631
+ }
1632
+ const orderResult = await clients.swap.pse.create({
1633
+ rateSig: rate.sig,
1634
+ toMedium: "kusama",
1635
+ amountSrc: rawAmount,
1636
+ webhookUrl,
1637
+ depositInformation: { urn: accountUrn },
1638
+ args: {
1639
+ bankCode,
1640
+ userType,
1641
+ customerEmail,
1642
+ userLegalIdType,
1643
+ userLegalId,
1644
+ customerData: { fullName, phoneNumber: phoneNumber ?? "" }
1645
+ }
1646
+ });
1647
+ const checkoutUrl = orderResult.execution?.result?.how?.url;
1648
+ const result = {
1649
+ order: {
1650
+ id: orderResult.order.id,
1651
+ fromAmount: orderResult.order.fromAmount,
1652
+ toAmount: orderResult.order.toAmount,
1653
+ status: orderResult.order.status
1654
+ },
1655
+ checkoutUrl,
1656
+ rate: { ratio: rate.ratio, fee: rate.fee }
1657
+ };
1658
+ return {
1659
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1660
+ };
1661
+ }
1662
+ );
1663
+ server.registerTool(
1664
+ "cashout_to_bank",
1665
+ {
1666
+ description: "Cash out USD to Colombian pesos via bank transfer. Converts from a Bloque account and deposits COP into any Colombian bank account. Handles rate lookup and order creation.",
1667
+ inputSchema: {
1668
+ sourceAccountUrn: z12.string(),
1669
+ amount: z12.string(),
1670
+ currency: z12.string().optional().default("USD"),
1671
+ bankName: z12.string(),
1672
+ bankAccountType: z12.enum(["savings", "checking"]),
1673
+ bankAccountNumber: z12.string(),
1674
+ bankAccountHolderName: z12.string(),
1675
+ idType: z12.enum(["CC", "CE", "NIT", "PP"]),
1676
+ idNumber: z12.string(),
1677
+ webhookUrl: z12.string().optional()
1678
+ }
1679
+ },
1680
+ async ({
1681
+ sourceAccountUrn,
1682
+ amount,
1683
+ currency,
1684
+ bankName,
1685
+ bankAccountType,
1686
+ bankAccountNumber,
1687
+ bankAccountHolderName,
1688
+ idType,
1689
+ idNumber,
1690
+ webhookUrl
1691
+ }) => {
1692
+ const { amount: rawAmount } = toRaw(amount, currency);
1693
+ const ratesResult = await clients.swap.findRates({
1694
+ fromAsset: "DUSD/6",
1695
+ toAsset: "COP/2",
1696
+ fromMediums: ["kusama"],
1697
+ toMediums: [bankName],
1698
+ amountSrc: rawAmount
1699
+ });
1700
+ const rate = ratesResult.rates[0];
1701
+ if (!rate) {
1702
+ return {
1703
+ content: [{
1704
+ type: "text",
1705
+ text: JSON.stringify(
1706
+ { error: `No exchange rates available for USD \u2192 COP via ${bankName}` },
1707
+ null,
1708
+ 2
1709
+ )
1710
+ }]
1711
+ };
1712
+ }
1713
+ const orderResult = await clients.swap.bankTransfer.create({
1714
+ rateSig: rate.sig,
1715
+ toMedium: bankName,
1716
+ amountSrc: rawAmount,
1717
+ webhookUrl,
1718
+ depositInformation: {
1719
+ bankAccountType,
1720
+ bankAccountNumber,
1721
+ bankAccountHolderName,
1722
+ bankAccountHolderIdentificationType: idType,
1723
+ bankAccountHolderIdentificationValue: idNumber
1724
+ },
1725
+ args: { sourceAccountUrn }
1726
+ });
1727
+ const result = {
1728
+ order: {
1729
+ id: orderResult.order.id,
1730
+ fromAmount: orderResult.order.fromAmount,
1731
+ toAmount: orderResult.order.toAmount,
1732
+ status: orderResult.order.status
1733
+ },
1734
+ rate: { ratio: rate.ratio, fee: rate.fee }
1735
+ };
1736
+ return {
1737
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1738
+ };
1739
+ }
1740
+ );
1741
+ }
1742
+
1743
+ // src/mcp/tools/workflows/spending-rules.ts
1744
+ import { z as z13 } from "zod/v4";
1745
+ function registerSpendingRulesWorkflows(server, clients) {
1746
+ server.registerTool(
1747
+ "configure_spending_rules",
1748
+ {
1749
+ description: "Advanced: configure smart spending rules that route a card's transactions across MULTIPLE accounts based on merchant category. Each route maps categories/MCCs to an account. Purchases are checked in route order; first match is debited. Routes without categories act as catch-all. Use create_card with allowedCategories for simple restrictions; use this only when one card needs to draw from multiple balance pools.",
1750
+ inputSchema: {
1751
+ cardUrn: z13.string(),
1752
+ routes: z13.array(
1753
+ z13.object({
1754
+ accountUrn: z13.string(),
1755
+ categories: z13.array(z13.string()).optional(),
1756
+ mccs: z13.array(z13.string()).optional()
1757
+ })
1758
+ )
1759
+ }
1760
+ },
1761
+ async ({ cardUrn, routes }) => {
1762
+ const allMccs = [];
1763
+ const priorityMcc = {};
1764
+ let defaultAccountUrn;
1765
+ for (const route of routes) {
1766
+ const resolved = resolveMccs(route.categories, route.mccs);
1767
+ if (resolved.length === 0) {
1768
+ defaultAccountUrn = route.accountUrn;
1769
+ continue;
1770
+ }
1771
+ for (const mcc of resolved) {
1772
+ allMccs.push(mcc);
1773
+ priorityMcc[mcc] = { asset: "DUSD/6", account_urn: route.accountUrn };
1774
+ }
1775
+ }
1776
+ const metadata = {
1777
+ spending_control: "smart",
1778
+ preferred_asset: "DUSD/6",
1779
+ default_asset: "DUSD/6",
1780
+ mcc_whitelist: [...new Set(allMccs)],
1781
+ priority_mcc: priorityMcc
1782
+ };
1783
+ if (defaultAccountUrn) {
1784
+ metadata.default_account_urn = defaultAccountUrn;
1785
+ }
1786
+ const updatedCard = await clients.accounts.card.updateMetadata({
1787
+ urn: cardUrn,
1788
+ metadata
1789
+ });
1790
+ const summary = routes.map((route) => {
1791
+ const resolved = resolveMccs(route.categories, route.mccs);
1792
+ return {
1793
+ accountUrn: route.accountUrn,
1794
+ categories: route.categories ?? [],
1795
+ mccs: resolved,
1796
+ isCatchAll: resolved.length === 0
1797
+ };
1798
+ });
1799
+ const result = { updatedCard, configuredRoutes: summary };
1800
+ return {
1801
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1802
+ };
1803
+ }
1804
+ );
1805
+ server.registerTool(
1806
+ "add_spending_category",
1807
+ {
1808
+ description: "Add a new spending category to a card's smart routing. Creates a new account for the category and wires it into the routing rules. If the card doesn't have smart routing yet, it is automatically enabled.",
1809
+ inputSchema: {
1810
+ cardUrn: z13.string(),
1811
+ categoryName: z13.string(),
1812
+ categories: z13.array(z13.string()).optional(),
1813
+ mccs: z13.array(z13.string()).optional(),
1814
+ fundFromUrn: z13.string().optional(),
1815
+ fundAmount: z13.string().optional(),
1816
+ currency: z13.string().optional().default("USD")
1817
+ }
1818
+ },
1819
+ async ({ cardUrn, categoryName, categories, mccs, fundFromUrn, fundAmount, currency }) => {
1820
+ const pocket = await clients.accounts.virtual.create({ name: categoryName });
1821
+ const polygon = await clients.accounts.polygon.create({
1822
+ ledgerId: pocket.ledgerId,
1823
+ name: categoryName
1824
+ });
1825
+ const card = await clients.accounts.get(cardUrn);
1826
+ const existing = card.metadata ?? {};
1827
+ const newMccs = resolveMccs(categories, mccs);
1828
+ const existingWhitelist = existing.mcc_whitelist ?? [];
1829
+ const existingPriority = existing.priority_mcc ?? {};
1830
+ const mergedWhitelist = [.../* @__PURE__ */ new Set([...existingWhitelist, ...newMccs])];
1831
+ const mergedPriority = { ...existingPriority };
1832
+ for (const mcc of newMccs) {
1833
+ mergedPriority[mcc] = { asset: "DUSD/6", account_urn: pocket.urn };
1834
+ }
1835
+ const mergedMetadata = {
1836
+ ...existing,
1837
+ spending_control: "smart",
1838
+ preferred_asset: "DUSD/6",
1839
+ default_asset: "DUSD/6",
1840
+ mcc_whitelist: mergedWhitelist,
1841
+ priority_mcc: mergedPriority
1842
+ };
1843
+ const updatedCard = await clients.accounts.card.updateMetadata({
1844
+ urn: cardUrn,
1845
+ metadata: mergedMetadata
1846
+ });
1847
+ let transferResult;
1848
+ if (fundFromUrn && fundAmount) {
1849
+ const { amount: rawAmount, asset } = toRaw(fundAmount, currency);
1850
+ transferResult = await clients.accounts.transfer({
1851
+ sourceUrn: fundFromUrn,
1852
+ destinationUrn: pocket.urn,
1853
+ amount: rawAmount,
1854
+ asset
1855
+ });
1856
+ }
1857
+ const result = {
1858
+ account: { urn: pocket.urn, ledgerId: pocket.ledgerId },
1859
+ polygon: { urn: polygon.urn, address: polygon.address },
1860
+ updatedCard,
1861
+ funded: !!transferResult,
1862
+ transferResult
1863
+ };
1864
+ return {
1865
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1866
+ };
1867
+ }
1868
+ );
1869
+ }
1870
+
1871
+ // src/mcp/tools/workflows/overview.ts
1872
+ import { z as z14 } from "zod/v4";
1873
+ function registerOverviewWorkflows(server, clients) {
1874
+ server.registerTool(
1875
+ "wallet_overview",
1876
+ {
1877
+ description: "Get a complete financial overview in a single call. Returns all accounts grouped by type, with polygon addresses and attached cards, aggregated balances, and recent transactions. Use as the starting point when the user asks 'what do I have?'."
1878
+ },
1879
+ async () => {
1880
+ const { accounts } = await clients.accounts.list();
1881
+ const rawBalances = await clients.accounts.balances();
1882
+ const txs = await clients.accounts.transactions({ limit: 10 });
1883
+ const grouped = {
1884
+ cards: [],
1885
+ virtual: [],
1886
+ polygon: [],
1887
+ us: [],
1888
+ bancolombia: []
1889
+ };
1890
+ for (const account of accounts) {
1891
+ const humanized = {
1892
+ ...account,
1893
+ balance: account.balance ? humanizeBalance(account.balance) : void 0
1894
+ };
1895
+ switch (account.medium) {
1896
+ case "card":
1897
+ grouped.cards.push(humanized);
1898
+ break;
1899
+ case "virtual":
1900
+ grouped.virtual.push(humanized);
1901
+ break;
1902
+ case "polygon":
1903
+ grouped.polygon.push(humanized);
1904
+ break;
1905
+ case "us-account":
1906
+ grouped.us.push(humanized);
1907
+ break;
1908
+ case "bancolombia":
1909
+ grouped.bancolombia.push(humanized);
1910
+ break;
1911
+ default:
1912
+ if (!grouped[account.medium]) grouped[account.medium] = [];
1913
+ grouped[account.medium].push(humanized);
1914
+ }
1915
+ }
1916
+ const humanizedTxs = txs.data.map((tx) => {
1917
+ const { amount, currency } = toHuman(tx.amount, tx.asset);
1918
+ return { ...tx, amount, currency };
1919
+ });
1920
+ const result = {
1921
+ accounts: grouped,
1922
+ totalBalances: humanizeBalance(rawBalances),
1923
+ recentTransactions: humanizedTxs
1924
+ };
1925
+ return {
1926
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1927
+ };
1928
+ }
1929
+ );
1930
+ server.registerTool(
1931
+ "card_summary",
1932
+ {
1933
+ description: "Get everything about a specific card: details (lastFour, status, detailsUrl), backing account, polygon address, balance, MCC restrictions, smart routing config, and recent movements. The detailsUrl is a PCI-compliant link for the USER to open to view full card number/CVV/expiry \u2014 the agent cannot read these.",
1934
+ inputSchema: {
1935
+ cardUrn: z14.string(),
1936
+ movementsLimit: z14.number().optional().default(10)
1937
+ }
1938
+ },
1939
+ async ({ cardUrn, movementsLimit }) => {
1940
+ const card = await clients.accounts.get(cardUrn);
1941
+ const rawBalance = await clients.accounts.balance(cardUrn);
1942
+ let polygonAccount = null;
1943
+ try {
1944
+ const { accounts: polygonAccounts } = await clients.accounts.list({ medium: "polygon" });
1945
+ const match = polygonAccounts.find((a) => a.ledgerId === card.ledgerId);
1946
+ if (match) {
1947
+ polygonAccount = {
1948
+ urn: match.urn,
1949
+ address: match.details?.address ?? match.address,
1950
+ network: match.details?.network ?? match.network
1951
+ };
1952
+ }
1953
+ } catch {
1954
+ }
1955
+ let movements = [];
1956
+ let movementsMeta = {};
1957
+ try {
1958
+ const mvResult = await clients.accounts.movements({ urn: cardUrn, limit: movementsLimit });
1959
+ movements = mvResult.data;
1960
+ movementsMeta = {
1961
+ pageSize: mvResult.pageSize,
1962
+ hasMore: mvResult.hasMore,
1963
+ next: mvResult.next
1964
+ };
1965
+ } catch {
1966
+ }
1967
+ const details = card.details ?? card;
1968
+ const metadata = card.metadata ?? {};
1969
+ const humanizedMovements = movements.map((mv) => {
1970
+ const { amount, currency } = toHuman(mv.amount, mv.asset);
1971
+ return { ...mv, amount, currency };
1972
+ });
1973
+ const result = {
1974
+ card: {
1975
+ urn: card.urn,
1976
+ lastFour: details.lastFour ?? details.last_four,
1977
+ status: details.status ?? card.status,
1978
+ detailsUrl: details.detailsUrl ?? details.details_url,
1979
+ cardType: details.cardType ?? details.card_type,
1980
+ ledgerId: card.ledgerId
1981
+ },
1982
+ account: null,
1983
+ polygonAddress: polygonAccount?.address ?? null,
1984
+ balance: humanizeBalance(rawBalance),
1985
+ restrictions: {
1986
+ mccWhitelist: metadata.mcc_whitelist ?? null,
1987
+ spendingControl: metadata.spending_control ?? null
1988
+ },
1989
+ smartRouting: metadata.priority_mcc ?? null,
1990
+ recentMovements: {
1991
+ data: humanizedMovements,
1992
+ ...movementsMeta
1993
+ }
1994
+ };
1995
+ return {
1996
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1997
+ };
1998
+ }
1999
+ );
2000
+ }
2001
+
2002
+ // src/mcp/server.ts
2003
+ function createBloqueServer(clients) {
2004
+ const server = new McpServer({ name: "bloque", version: "0.0.1" });
2005
+ registerAccountTools(server, clients);
2006
+ registerVirtualTools(server, clients);
2007
+ registerCardTools(server, clients);
2008
+ registerPolygonTools(server, clients);
2009
+ registerUsTools(server, clients);
2010
+ registerTransferTools(server, clients);
2011
+ registerHistoryTools(server, clients);
2012
+ registerSwapTools(server, clients);
2013
+ registerIdentityWorkflows(server, clients);
2014
+ registerAccountWorkflows(server, clients);
2015
+ registerCardWorkflows(server, clients);
2016
+ registerFundCardWorkflows(server, clients);
2017
+ registerTopupWorkflows(server, clients);
2018
+ registerSpendingRulesWorkflows(server, clients);
2019
+ registerOverviewWorkflows(server, clients);
2020
+ return server;
2021
+ }
2022
+ async function startServer(server, mode = "stdio", options) {
2023
+ if (mode === "stdio") {
2024
+ const transport2 = new StdioServerTransport();
2025
+ await server.connect(transport2);
2026
+ return;
2027
+ }
2028
+ const port = options?.port ?? 3100;
2029
+ const transport = new StreamableHTTPServerTransport({
2030
+ sessionIdGenerator: () => randomUUID(),
2031
+ enableJsonResponse: true
2032
+ });
2033
+ await server.connect(transport);
2034
+ const httpServer = createServer((req, res) => {
2035
+ if (req.url === "/mcp") {
2036
+ transport.handleRequest(req, res);
2037
+ } else {
2038
+ res.writeHead(404).end("Not Found");
2039
+ }
2040
+ });
2041
+ httpServer.listen(port, () => {
2042
+ console.error(`Bloque MCP server listening on http://localhost:${port}/mcp`);
2043
+ });
2044
+ }
2045
+
2046
+ // src/commands/mcp.ts
2047
+ var store4 = new SessionStore();
2048
+ var mcpCommand = new Command4("mcp").description("Start the MCP server exposing Bloque SDK tools").option("--http", "Use HTTP/SSE transport instead of stdio").option("--port <port>", "HTTP port (only with --http)", "3100").action(async (opts) => {
2049
+ const session = store4.load();
2050
+ if (!session) {
2051
+ console.error("Not logged in. Run `bloque login` first.");
2052
+ process.exit(1);
2053
+ }
2054
+ const sdkConfig = session.authType === "apiKey" && session.apiKey ? {
2055
+ auth: { type: "apiKey", apiKey: session.apiKey },
2056
+ mode: session.mode,
2057
+ origin: session.origin
2058
+ } : {
2059
+ auth: { type: "jwt" },
2060
+ mode: session.mode,
2061
+ origin: session.origin,
2062
+ tokenStorage: {
2063
+ get: () => session.accessToken,
2064
+ set: () => {
2065
+ },
2066
+ clear: () => {
2067
+ }
2068
+ }
2069
+ };
2070
+ const sdk = new SDK2(sdkConfig);
2071
+ let clients;
2072
+ if (session.authType === "apiKey" && session.alias) {
2073
+ clients = await sdk.connect(session.alias);
2074
+ } else {
2075
+ clients = await sdk.authenticate();
2076
+ }
2077
+ const server = createBloqueServer(clients);
2078
+ if (opts.http) {
2079
+ await startServer(server, "http", { port: Number(opts.port) });
2080
+ } else {
2081
+ await startServer(server, "stdio");
2082
+ }
2083
+ });
2084
+
2085
+ // src/commands/setup.ts
2086
+ import fs2 from "fs";
2087
+ import os2 from "os";
2088
+ import path2 from "path";
2089
+ import { execSync } from "child_process";
2090
+ import { Command as Command5 } from "commander";
2091
+ import { SDK as SDK3 } from "@bloque/sdk";
2092
+ import { checkbox, confirm, input as input2, password as password2, select as select2 } from "@inquirer/prompts";
2093
+ var store5 = new SessionStore();
2094
+ var OTP_CHANNELS2 = [
2095
+ {
2096
+ name: "WhatsApp",
2097
+ value: "bloque-whatsapp",
2098
+ prompt: "Phone number (e.g. +573001234567):",
2099
+ sentMessage: "OTP sent via WhatsApp \u2014 check your phone."
2100
+ },
2101
+ {
2102
+ name: "Email",
2103
+ value: "bloque-email",
2104
+ prompt: "Email address:",
2105
+ sentMessage: "OTP sent \u2014 check your inbox."
2106
+ }
2107
+ ];
2108
+ var PLATFORM = process.platform;
2109
+ var HOME = os2.homedir();
2110
+ var APPDATA = process.env.APPDATA || path2.join(HOME, "AppData", "Roaming");
2111
+ var LOCAL_APPDATA = process.env.LOCALAPPDATA || path2.join(HOME, "AppData", "Local");
2112
+ function hasCommand(bin) {
2113
+ try {
2114
+ const cmd = PLATFORM === "win32" ? `where ${bin} 2>nul` : `which ${bin} 2>/dev/null`;
2115
+ execSync(cmd, { stdio: "pipe" });
2116
+ return true;
2117
+ } catch {
2118
+ return false;
2119
+ }
2120
+ }
2121
+ function hasMacApp(bundleId) {
2122
+ if (PLATFORM !== "darwin") return false;
2123
+ try {
2124
+ const result = execSync(
2125
+ `mdfind "kMDItemCFBundleIdentifier == '${bundleId}'" 2>/dev/null`,
2126
+ { stdio: "pipe", encoding: "utf-8" }
2127
+ );
2128
+ return result.trim().length > 0;
2129
+ } catch {
2130
+ return false;
2131
+ }
2132
+ }
2133
+ function hasWindowsApp(...relativePaths) {
2134
+ if (PLATFORM !== "win32") return false;
2135
+ return relativePaths.some((p) => fs2.existsSync(path2.join(LOCAL_APPDATA, p)));
2136
+ }
2137
+ function hasLinuxDesktop(desktopName) {
2138
+ if (PLATFORM !== "linux") return false;
2139
+ const dirs = [
2140
+ "/usr/share/applications",
2141
+ path2.join(HOME, ".local", "share", "applications"),
2142
+ "/var/lib/snapd/desktop/applications"
2143
+ ];
2144
+ return dirs.some((d) => fs2.existsSync(path2.join(d, desktopName)));
2145
+ }
2146
+ function claudeDesktopConfigPath() {
2147
+ if (PLATFORM === "win32")
2148
+ return path2.join(APPDATA, "Claude", "claude_desktop_config.json");
2149
+ if (PLATFORM === "linux")
2150
+ return path2.join(HOME, ".config", "Claude", "claude_desktop_config.json");
2151
+ return path2.join(HOME, "Library", "Application Support", "Claude", "claude_desktop_config.json");
2152
+ }
2153
+ var AGENTS = [
2154
+ {
2155
+ id: "cursor",
2156
+ name: "Cursor",
2157
+ configPath: path2.join(HOME, ".cursor", "mcp.json"),
2158
+ skillAgent: "cursor",
2159
+ detect: () => hasMacApp("com.todesktop.230313mzl4w4u92") || hasWindowsApp("Programs\\cursor\\Cursor.exe") || hasLinuxDesktop("cursor.desktop") || hasCommand("cursor") || fs2.existsSync(path2.join(HOME, ".cursor"))
2160
+ },
2161
+ {
2162
+ id: "claude-desktop",
2163
+ name: "Claude Desktop",
2164
+ configPath: claudeDesktopConfigPath(),
2165
+ detect: () => hasMacApp("com.anthropic.claudefordesktop") || hasWindowsApp("AnthropicClaude\\Claude.exe", "Programs\\Claude\\Claude.exe") || hasLinuxDesktop("claude-desktop.desktop") || hasCommand("claude-desktop")
2166
+ },
2167
+ {
2168
+ id: "claude-code",
2169
+ name: "Claude Code",
2170
+ configPath: path2.join(HOME, ".claude.json"),
2171
+ skillAgent: "claude-code",
2172
+ detect: () => hasCommand("claude")
2173
+ },
2174
+ {
2175
+ id: "antigravity",
2176
+ name: "Antigravity (Google)",
2177
+ configPath: path2.join(HOME, ".gemini", "antigravity", "mcp_config.json"),
2178
+ skillAgent: "gemini-cli",
2179
+ detect: () => hasCommand("gemini")
2180
+ }
2181
+ ];
2182
+ var MCP_ENTRY = {
2183
+ command: "npx",
2184
+ args: ["-y", "@bloque/cli", "mcp"]
2185
+ };
2186
+ function detectInstalled() {
2187
+ return AGENTS.filter((agent) => {
2188
+ try {
2189
+ return agent.detect();
2190
+ } catch {
2191
+ return false;
2192
+ }
2193
+ });
2194
+ }
2195
+ function readJsonFile(filePath) {
2196
+ try {
2197
+ const raw = fs2.readFileSync(filePath, "utf-8");
2198
+ return JSON.parse(raw);
2199
+ } catch {
2200
+ return {};
2201
+ }
2202
+ }
2203
+ function writeJsonFile(filePath, data) {
2204
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
2205
+ fs2.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
2206
+ }
2207
+ function injectMcpConfig(agent) {
2208
+ const config = readJsonFile(agent.configPath);
2209
+ if (!config.mcpServers) {
2210
+ config.mcpServers = {};
2211
+ }
2212
+ if (config.mcpServers.bloque) {
2213
+ return { action: "exists" };
2214
+ }
2215
+ config.mcpServers.bloque = MCP_ENTRY;
2216
+ writeJsonFile(agent.configPath, config);
2217
+ return { action: "added" };
2218
+ }
2219
+ function installSkills(agents) {
2220
+ const skillAgents = agents.filter((a) => a.skillAgent).map((a) => a.skillAgent);
2221
+ if (skillAgents.length === 0) return false;
2222
+ const agentFlags = skillAgents.map((a) => `-a ${a}`).join(" ");
2223
+ try {
2224
+ execSync(
2225
+ `npx -y skills add bloque-app/skills --skill bloque-sdk-ts -g ${agentFlags} -y`,
2226
+ { stdio: "pipe", timeout: 12e4 }
2227
+ );
2228
+ return true;
2229
+ } catch {
2230
+ return false;
2231
+ }
2232
+ }
2233
+ async function runOtpLogin(mode) {
2234
+ const channel = await select2({
2235
+ message: "How would you like to receive your OTP?",
2236
+ choices: OTP_CHANNELS2.map((c) => ({ name: c.name, value: c.value }))
2237
+ });
2238
+ const selectedChannel = OTP_CHANNELS2.find((c) => c.value === channel);
2239
+ const userAlias = await input2({ message: selectedChannel.prompt });
2240
+ let _token = null;
2241
+ const sdk = new SDK3({
2242
+ auth: { type: "jwt" },
2243
+ mode,
2244
+ origin: channel,
2245
+ tokenStorage: {
2246
+ get: () => _token,
2247
+ set: (t) => {
2248
+ _token = t;
2249
+ },
2250
+ clear: () => {
2251
+ _token = null;
2252
+ }
2253
+ }
2254
+ });
2255
+ await sdk.assert(channel, userAlias);
2256
+ console.log(selectedChannel.sentMessage);
2257
+ const code = await password2({ message: "Enter OTP code:", mask: "*" });
2258
+ const clients = await sdk.connect(channel, userAlias, code);
2259
+ store5.save({
2260
+ accessToken: clients.accessToken,
2261
+ urn: clients.urn ?? "",
2262
+ origin: channel,
2263
+ mode,
2264
+ authType: "jwt",
2265
+ alias: userAlias,
2266
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2267
+ });
2268
+ await portalAnimation(`Connected as ${clients.urn ?? userAlias}`);
2269
+ }
2270
+ var setupCommand = new Command5("setup").description("Set up Bloque MCP in your AI code agents").option("--jwt <token>", "JWT token for authentication (skips OTP)").option("--sandbox", "Use sandbox environment instead of production").action(async (opts) => {
2271
+ const { jwt, sandbox } = opts;
2272
+ const mode = sandbox ? "sandbox" : "production";
2273
+ console.log("\n Bloque Setup Wizard\n");
2274
+ if (jwt) {
2275
+ store5.save({
2276
+ accessToken: jwt,
2277
+ urn: "",
2278
+ origin: "cli",
2279
+ mode,
2280
+ authType: "jwt",
2281
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2282
+ });
2283
+ console.log(" Session saved from JWT token.\n");
2284
+ } else if (store5.exists()) {
2285
+ const session = store5.load();
2286
+ console.log(` Already logged in (${session.alias || session.urn || "session found"}).`);
2287
+ const reuse = await confirm({ message: "Use existing session?", default: true });
2288
+ if (!reuse) {
2289
+ await runOtpLogin(mode);
2290
+ } else {
2291
+ console.log();
2292
+ }
2293
+ } else {
2294
+ console.log(" No session found. Let's log in first.\n");
2295
+ await runOtpLogin(mode);
2296
+ }
2297
+ const detected = detectInstalled();
2298
+ const detectedIds = new Set(detected.map((a) => a.id));
2299
+ if (AGENTS.length === 0) {
2300
+ console.log(" No supported AI agents found on this system.");
2301
+ return;
2302
+ }
2303
+ const choices = AGENTS.map((agent) => ({
2304
+ name: detectedIds.has(agent.id) ? `${agent.name} (detected)` : `${agent.name}`,
2305
+ value: agent.id,
2306
+ checked: detectedIds.has(agent.id)
2307
+ }));
2308
+ const selectedIds = await checkbox({
2309
+ message: "Which agents should Bloque be installed in?",
2310
+ choices
2311
+ });
2312
+ if (selectedIds.length === 0) {
2313
+ console.log("\n No agents selected. Setup complete.\n");
2314
+ return;
2315
+ }
2316
+ const selectedAgents = AGENTS.filter((a) => selectedIds.includes(a.id));
2317
+ console.log("\n Installing MCP server config...\n");
2318
+ const mcpResults = [];
2319
+ for (const agent of selectedAgents) {
2320
+ const { action } = injectMcpConfig(agent);
2321
+ if (action === "exists") {
2322
+ const overwrite = await confirm({
2323
+ message: ` ${agent.name}: bloque MCP already configured. Overwrite?`,
2324
+ default: false
2325
+ });
2326
+ if (overwrite) {
2327
+ const config = readJsonFile(agent.configPath);
2328
+ config.mcpServers.bloque = MCP_ENTRY;
2329
+ writeJsonFile(agent.configPath, config);
2330
+ mcpResults.push({ agent: agent.name, result: "overwritten" });
2331
+ } else {
2332
+ mcpResults.push({ agent: agent.name, result: "skipped (already configured)" });
2333
+ }
2334
+ } else {
2335
+ mcpResults.push({ agent: agent.name, result: "added" });
2336
+ }
2337
+ }
2338
+ for (const r of mcpResults) {
2339
+ console.log(` ${r.agent}: ${r.result}`);
2340
+ }
2341
+ const agentsWithSkill = selectedAgents.filter((a) => a.skillAgent);
2342
+ if (agentsWithSkill.length > 0) {
2343
+ const names = agentsWithSkill.map((a) => a.name).join(", ");
2344
+ process.stdout.write(`
2345
+ Installing Bloque SDK skill globally for ${names}...`);
2346
+ const ok = installSkills(agentsWithSkill);
2347
+ console.log(ok ? " done\n" : " failed (you can install manually later)\n");
2348
+ }
2349
+ await portalAnimation("Setup complete");
2350
+ console.log(" What was configured:\n");
2351
+ for (const r of mcpResults) {
2352
+ console.log(` \x1B[38;5;75m\u25B8\x1B[0m ${r.agent}: MCP server ${r.result}`);
2353
+ }
2354
+ if (agentsWithSkill.length > 0) {
2355
+ console.log(` \x1B[38;5;75m\u25B8\x1B[0m Bloque SDK skill: ${agentsWithSkill.map((a) => a.name).join(", ")}`);
2356
+ }
2357
+ console.log("\n Next steps:\n");
2358
+ console.log(" 1. Restart your AI agent(s) for changes to take effect");
2359
+ console.log(" 2. Ask your agent to use the Bloque tools");
2360
+ console.log(" 3. Run `bloque mcp --http` if you need HTTP transport\n");
2361
+ });
2362
+
2363
+ // src/bin.ts
2364
+ var program = new Command6();
2365
+ program.name("bloque").description("Bloque CLI \u2014 manage accounts and cards via MCP").version("0.0.1");
2366
+ program.addCommand(setupCommand);
2367
+ program.addCommand(loginCommand);
2368
+ program.addCommand(logoutCommand);
2369
+ program.addCommand(whoamiCommand);
2370
+ program.addCommand(mcpCommand);
2371
+ program.parse();