@quackai/q402-mcp 0.3.7 → 0.3.9

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 +5 -4
  2. package/dist/index.js +296 -37
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -5,9 +5,9 @@
5
5
  [![npm](https://img.shields.io/npm/v/@quackai/q402-mcp.svg)](https://www.npmjs.com/package/@quackai/q402-mcp)
6
6
  [![license](https://img.shields.io/npm/l/@quackai/q402-mcp.svg)](./LICENSE)
7
7
 
8
- > **🎟️ Free trial available (2026-05-13 → 2026-05-20)** — 2,000 gasless transactions on BNB Chain (USDC + USDT), 30-day window, no card. One wallet signature: <https://q402.quackai.ai>.
8
+ > **🎟️ Free trial available (2026-05-19 → 2026-06-30)** — 2,000 gasless transactions on BNB Chain (USDC + USDT), 30-day window, no card. One wallet signature: <https://q402.quackai.ai>.
9
9
  >
10
- > **BNB-focus sprint window:** while the sprint is on, `q402_pay` accepts `chain: "bnb"` with `token: "USDC"` or `"USDT"` only; the other 6 chains and RLUSD return after the sprint. `q402_quote` is narrowed to BNB Chain accordingly. Source-of-truth flag: [`feature-flags.ts`](https://github.com/bitgett/Q402-Institutional/blob/feat/bnb-focus-sprint/app/lib/feature-flags.ts).
10
+ > **Trial-scope policy:** API keys minted under the free-trial program (`plan: "trial"`) are restricted to BNB Chain with USDC/USDT server-side enforcement, returns `403 TRIAL_BNB_ONLY` otherwise. **Paid API keys see the full 7-chain matrix at all times.**
11
11
 
12
12
  Claude can now reason about stablecoin payments end to end — quote a transfer across 7 chains, pick the cheapest route, and (optionally) settle the transaction over [Q402](https://q402.quackai.ai)'s EIP-7702 relayer infrastructure. The recipient receives the full amount; the sender pays $0 in gas.
13
13
 
@@ -46,10 +46,11 @@ You'll get a ranked breakdown immediately — no API key, no signup, no funds at
46
46
  |---|---|---|
47
47
  | `q402_quote` | none | Compare gas cost and supported tokens across chains. Read-only. |
48
48
  | `q402_balance` | API key | Verify the API key and report its plan tier + remaining quota credits (live vs sandbox). |
49
- | `q402_pay` | API key + private key + flag | Send a gasless payment. **Sandbox by default** — see [Sandbox vs live mode](#sandbox-vs-live-mode). |
49
+ | `q402_pay` | API key + private key + flag | Send a gasless payment to a single recipient. **Sandbox by default** — see [Sandbox vs live mode](#sandbox-vs-live-mode). |
50
+ | `q402_batch_pay` | API key + private key + flag | Send a gasless payment to **multiple** recipients in one call on a single chain × token. Trial keys: 5 rows max. Paid keys: 20 rows max. Same sandbox gating as `q402_pay`. |
50
51
  | `q402_receipt` | none | Look up a Trust Receipt by `rct_…` id and locally verify its ECDSA signature against the relayer EOA. Returns the public settlement record + a `verified` boolean. *receiptId-only today; tx-hash lookup reserved for a future release.* |
51
52
 
52
- `q402_pay` follows a "confirm in chat first" contract: the tool description instructs the model to never call it without explicit user approval of the recipient address, amount, chain, and token.
53
+ `q402_pay` and `q402_batch_pay` follow a "confirm in chat first" contract: the tool description instructs the model to never call it without explicit user approval of the recipient address(es), amount(s), chain, and token. For batch calls the user must approve the **full batch**, not the individual rows.
53
54
 
54
55
  `q402_receipt` is the natural follow-up: after `q402_pay` returns a `receiptUrl`, hand the agent the `rct_…` id and ask *"verify this receipt"* — the tool re-runs the same canonical-JSON + EIP-191 recovery the receipt page does in the browser, so the verification doesn't depend on trusting any UI. Example prompts that work today:
55
56
 
package/dist/index.js CHANGED
@@ -431,6 +431,116 @@ var Q402NodeClient = class _Q402NodeClient {
431
431
  data.explorerUrl = _Q402NodeClient.explorerUrl(chain, data.txHash);
432
432
  return data;
433
433
  }
434
+ /**
435
+ * Multi-recipient settlement on a single chain + token. Trial keys can
436
+ * fan out to at most 5 recipients per call; paid keys up to 20. The
437
+ * server enforces the cap and rejects oversized batches with
438
+ * `BATCH_TOO_LARGE`.
439
+ *
440
+ * Each recipient is independently authorised: one EIP-712
441
+ * TransferAuthorization witness + one EIP-7702 authorization tuple
442
+ * per row. The authorization nonces are issued sequentially starting
443
+ * from the EOA's current on-chain nonce, so the EVM applies them
444
+ * cleanly in batch order. Execution is sequential server-side; the
445
+ * first transfer must succeed (it installs / re-confirms the
446
+ * delegation), after which the remaining transfers are surfaced in
447
+ * the result array even if individual ones fail.
448
+ */
449
+ async batchPay(inputs) {
450
+ if (!Array.isArray(inputs) || inputs.length === 0) {
451
+ throw new Error("batchPay requires at least one recipient");
452
+ }
453
+ const { chain, relayBaseUrl, apiKey, privateKey } = this.opts;
454
+ for (let i = 0; i < inputs.length; i++) {
455
+ const inp = inputs[i];
456
+ const tokenCfg = tokenFor(chain, inp.token);
457
+ if (chain.supportedTokens && !chain.supportedTokens.includes(inp.token)) {
458
+ throw new Error(
459
+ `recipient[${i}]: token ${inp.token} is not supported on chain ${chain.key}. Supported: ${chain.supportedTokens.join(", ")}.`
460
+ );
461
+ }
462
+ try {
463
+ toRawAmount(inp.amount, tokenCfg.decimals);
464
+ } catch (e) {
465
+ throw new Error(
466
+ `recipient[${i}]: ${e instanceof Error ? e.message : String(e)}`
467
+ );
468
+ }
469
+ }
470
+ const rpcUrl = this.opts.rpcUrl ?? DEFAULT_RPC[chain.chainId];
471
+ if (!rpcUrl) {
472
+ throw new Error(
473
+ `no RPC URL configured for chain ${chain.key} (chainId ${chain.chainId})`
474
+ );
475
+ }
476
+ const provider = new JsonRpcProvider(rpcUrl);
477
+ const wallet = new Wallet(privateKey, provider);
478
+ const owner = await wallet.getAddress();
479
+ const facilitator = await this.fetchFacilitator();
480
+ const baseAuthNonce = await provider.getTransactionCount(owner);
481
+ const deadline = Math.floor(Date.now() / 1e3) + 600;
482
+ const recipients = [];
483
+ for (let i = 0; i < inputs.length; i++) {
484
+ const inp = inputs[i];
485
+ const tokenCfg = tokenFor(chain, inp.token);
486
+ const amountRaw = toRawAmount(inp.amount, tokenCfg.decimals);
487
+ const paymentNonce = toBigInt(randomBytes(32));
488
+ const witnessSig = await wallet.signTypedData(
489
+ {
490
+ name: chain.domainName,
491
+ version: "1",
492
+ chainId: chain.chainId,
493
+ verifyingContract: owner
494
+ },
495
+ TRANSFER_AUTH_TYPES,
496
+ {
497
+ owner,
498
+ facilitator,
499
+ token: tokenCfg.address,
500
+ recipient: inp.to,
501
+ amount: BigInt(amountRaw),
502
+ nonce: paymentNonce,
503
+ deadline: BigInt(deadline)
504
+ }
505
+ );
506
+ const authorization = await signAuthorization(wallet, {
507
+ chainId: chain.chainId,
508
+ address: chain.implContract,
509
+ nonce: baseAuthNonce + i
510
+ });
511
+ const baseRow = {
512
+ from: owner,
513
+ to: inp.to,
514
+ amount: amountRaw,
515
+ deadline,
516
+ witnessSig,
517
+ authorization
518
+ };
519
+ const row = chain.key === "xlayer" ? { ...baseRow, xlayerNonce: paymentNonce.toString() } : chain.key === "stable" ? { ...baseRow, stableNonce: paymentNonce.toString() } : { ...baseRow, nonce: paymentNonce.toString() };
520
+ recipients.push(row);
521
+ }
522
+ const resp = await fetch(`${relayBaseUrl.replace(/\/$/, "")}/relay/batch`, {
523
+ method: "POST",
524
+ headers: { "Content-Type": "application/json" },
525
+ body: JSON.stringify({
526
+ apiKey,
527
+ chain: chain.key,
528
+ token: inputs[0].token,
529
+ // all recipients must share token; pre-validated above
530
+ facilitator,
531
+ recipients
532
+ })
533
+ });
534
+ const data = await resp.json();
535
+ if (!resp.ok) {
536
+ throw new Error(data.error ?? `relay/batch failed (HTTP ${resp.status})`);
537
+ }
538
+ data.results = data.results.map((r) => ({
539
+ ...r,
540
+ ...r.success && r.txHash ? { explorerUrl: _Q402NodeClient.explorerUrl(chain, r.txHash) } : {}
541
+ }));
542
+ return data;
543
+ }
434
544
  };
435
545
  function sandboxPay(chain, input) {
436
546
  const tokenCfg = tokenFor(chain, input.token);
@@ -561,9 +671,154 @@ var PAY_TOOL = {
561
671
  }
562
672
  };
563
673
 
564
- // src/tools/balance.ts
674
+ // src/tools/batch-pay.ts
675
+ import { isAddress as isAddress3 } from "ethers";
565
676
  import { z as z3 } from "zod";
566
- var BalanceInputSchema = z3.object({});
677
+ var RECIPIENT_LIMIT_TRIAL = 5;
678
+ var RECIPIENT_LIMIT_PAID = 20;
679
+ var CLIENT_RECIPIENT_CAP = RECIPIENT_LIMIT_PAID;
680
+ var BatchPayInputSchema = z3.object({
681
+ chain: z3.enum(["avax", "bnb", "eth", "xlayer", "stable", "mantle", "injective"]),
682
+ token: z3.enum(["USDC", "USDT", "RLUSD"]).describe(
683
+ "Stablecoin symbol. USDC / USDT supported on most chains (Injective is USDT-only). RLUSD (Ripple USD, NY DFS regulated, decimals 18) is Ethereum-only. The same token applies to every recipient in the batch."
684
+ ),
685
+ recipients: z3.array(
686
+ z3.object({
687
+ to: z3.string().refine(isAddress3, "to must be a valid 0x-prefixed EVM address").describe("Recipient EVM address (0x + 40 hex)."),
688
+ amount: z3.string().regex(/^\d+(\.\d+)?$/, "amount must be a positive decimal string").describe('Human-readable decimal amount for this recipient, e.g. "5.00".')
689
+ })
690
+ ).min(1, "recipients must contain at least one row").max(CLIENT_RECIPIENT_CAP, `recipients cannot exceed ${CLIENT_RECIPIENT_CAP} (server enforces tighter cap by key scope)`).describe(
691
+ `Array of {to, amount} pairs. All recipients share the same chain and token. Trial keys: max ${RECIPIENT_LIMIT_TRIAL} rows. Paid keys: max ${RECIPIENT_LIMIT_PAID} rows.`
692
+ ),
693
+ confirm: z3.literal(true).describe(
694
+ "MUST be true. The user must have explicitly approved this exact set of recipients, amounts, chain, and token in the conversation right before this tool was called. Setting confirm=true on behalf of the user without that approval is a violation of the tool contract."
695
+ )
696
+ });
697
+ function maxAmountGuardBatch(recipients, cap) {
698
+ for (let i = 0; i < recipients.length; i++) {
699
+ const r = recipients[i];
700
+ const numeric = Number(r.amount);
701
+ if (!Number.isFinite(numeric)) {
702
+ throw new Error(`recipients[${i}]: unparseable amount "${r.amount}"`);
703
+ }
704
+ if (numeric > cap) {
705
+ throw new Error(
706
+ `recipients[${i}]: amount $${r.amount} exceeds the per-call cap of $${cap}. Set Q402_MAX_AMOUNT_PER_CALL to a higher value if intentional.`
707
+ );
708
+ }
709
+ }
710
+ }
711
+ function recipientAllowlistGuardBatch(recipients, allow) {
712
+ if (allow.length === 0) return;
713
+ for (let i = 0; i < recipients.length; i++) {
714
+ const to = recipients[i].to.toLowerCase();
715
+ if (!allow.includes(to)) {
716
+ throw new Error(
717
+ `recipients[${i}]: ${recipients[i].to} is not in Q402_ALLOWED_RECIPIENTS. Either add this address to the allowlist or unset the env var to disable the guard.`
718
+ );
719
+ }
720
+ }
721
+ }
722
+ async function runBatchPay(input) {
723
+ const chain = getChain(input.chain);
724
+ tokenFor(chain, input.token);
725
+ if (chain.supportedTokens && !chain.supportedTokens.includes(input.token)) {
726
+ throw new Error(
727
+ `token ${input.token} is not supported on chain ${chain.key}. Supported on this chain: ${chain.supportedTokens.join(", ")}.`
728
+ );
729
+ }
730
+ const guardsApplied = [];
731
+ maxAmountGuardBatch(input.recipients, CONFIG.maxAmountPerCallUsd);
732
+ guardsApplied.push(`max_amount<=${CONFIG.maxAmountPerCallUsd} (per recipient)`);
733
+ recipientAllowlistGuardBatch(input.recipients, CONFIG.allowedRecipients);
734
+ if (CONFIG.allowedRecipients.length > 0) {
735
+ guardsApplied.push(`recipient_allowlist[${CONFIG.allowedRecipients.length}]`);
736
+ }
737
+ if (CONFIG.mode === "sandbox") {
738
+ const sandboxResults = input.recipients.map(
739
+ (r) => sandboxPay(chain, { to: r.to, amount: r.amount, token: input.token })
740
+ );
741
+ guardsApplied.push("mode=sandbox");
742
+ return {
743
+ mode: "sandbox",
744
+ result: { sandbox: sandboxResults, reason: describeSandboxReason2() },
745
+ guardsApplied,
746
+ setupHint: describeSandboxReason2()
747
+ };
748
+ }
749
+ const client = new Q402NodeClient({
750
+ apiKey: CONFIG.apiKey,
751
+ privateKey: CONFIG.privateKey,
752
+ chain,
753
+ relayBaseUrl: CONFIG.relayBaseUrl
754
+ });
755
+ const result = await client.batchPay(
756
+ input.recipients.map((r) => ({ to: r.to, amount: r.amount, token: input.token }))
757
+ );
758
+ guardsApplied.push("mode=live");
759
+ guardsApplied.push(`scope=${result.scope} (server enforced)`);
760
+ guardsApplied.push(`batch_size=${input.recipients.length}/${result.limit}`);
761
+ return { mode: "live", result, guardsApplied };
762
+ }
763
+ function describeSandboxReason2() {
764
+ const missing = [];
765
+ if (CONFIG.apiKeyKind !== "live") missing.push("Q402_API_KEY (must start with q402_live_)");
766
+ if (!CONFIG.privateKey) missing.push("Q402_PRIVATE_KEY");
767
+ if (!CONFIG.realPaymentsRequested) missing.push("Q402_ENABLE_REAL_PAYMENTS=1");
768
+ if (missing.length === 0) return "Sandbox mode active (no env state change needed).";
769
+ return "Sandbox mode is active because the following env vars are missing or not yet set: " + missing.join(", ") + ". Get a live API key at https://q402.quackai.ai/dashboard.";
770
+ }
771
+ var BATCH_PAY_TOOL = {
772
+ name: "q402_batch_pay",
773
+ description: `Send gasless payments to MULTIPLE recipients on a single chain \xD7 token in one call. Trial keys (q402_live_* with plan='trial'): max ${RECIPIENT_LIMIT_TRIAL} recipients per call, BNB Chain + USDC/USDT only. Paid keys: max ${RECIPIENT_LIMIT_PAID} recipients per call, full 7-chain support. SANDBOX BY DEFAULT \u2014 real on-chain TX only when Q402_API_KEY (live), Q402_PRIVATE_KEY, and Q402_ENABLE_REAL_PAYMENTS=1 are all set. Every recipient receives the full amount; the sender pays $0 in gas for the entire batch. ALWAYS get explicit user confirmation of the complete recipient + amount list, chain, and token in conversation immediately before calling this tool \u2014 the user must approve the full batch, not the individual rows.`,
774
+ inputSchema: {
775
+ type: "object",
776
+ properties: {
777
+ chain: {
778
+ type: "string",
779
+ enum: CHAIN_KEYS,
780
+ description: "Target chain. Applies to every recipient in the batch."
781
+ },
782
+ token: {
783
+ type: "string",
784
+ enum: ["USDC", "USDT", "RLUSD"],
785
+ description: "Stablecoin for the entire batch. USDC / USDT supported on most chains; Injective is USDT-only; RLUSD (decimals 18) is Ethereum-only."
786
+ },
787
+ recipients: {
788
+ type: "array",
789
+ minItems: 1,
790
+ maxItems: CLIENT_RECIPIENT_CAP,
791
+ description: "List of recipients. Trial keys: max 5. Paid keys: max 20. Each item is {to, amount}.",
792
+ items: {
793
+ type: "object",
794
+ properties: {
795
+ to: {
796
+ type: "string",
797
+ description: "Recipient EVM address (0x + 40 hex)."
798
+ },
799
+ amount: {
800
+ type: "string",
801
+ description: 'Human-readable decimal amount for this recipient, e.g. "5.00".'
802
+ }
803
+ },
804
+ required: ["to", "amount"],
805
+ additionalProperties: false
806
+ }
807
+ },
808
+ confirm: {
809
+ type: "boolean",
810
+ const: true,
811
+ description: "MUST be true and only set after the user has confirmed the entire batch in chat."
812
+ }
813
+ },
814
+ required: ["chain", "token", "recipients", "confirm"],
815
+ additionalProperties: false
816
+ }
817
+ };
818
+
819
+ // src/tools/balance.ts
820
+ import { z as z4 } from "zod";
821
+ var BalanceInputSchema = z4.object({});
567
822
  function mask(key) {
568
823
  if (!key || key.length < 12) return null;
569
824
  return `${key.slice(0, 12)}\u2026${key.slice(-4)}`;
@@ -609,39 +864,39 @@ var BALANCE_TOOL = {
609
864
  };
610
865
 
611
866
  // src/tools/receipt.ts
612
- import { z as z4 } from "zod";
867
+ import { z as z5 } from "zod";
613
868
  import { keccak256, toUtf8Bytes, getBytes, verifyMessage } from "ethers";
614
- var ReceiptShape = z4.object({
615
- receiptId: z4.string(),
616
- createdAt: z4.string(),
617
- txHash: z4.string(),
618
- blockNumber: z4.number().optional(),
619
- chain: z4.string(),
620
- payer: z4.string(),
621
- recipient: z4.string(),
622
- token: z4.enum(["USDC", "USDT", "RLUSD"]),
623
- tokenAmount: z4.string(),
624
- tokenAmountRaw: z4.string(),
625
- method: z4.enum(["eip7702", "eip3009", "eip7702_xlayer", "eip7702_stable"]),
626
- gasCostNative: z4.string().optional(),
627
- apiKeyTier: z4.string().optional(),
869
+ var ReceiptShape = z5.object({
870
+ receiptId: z5.string(),
871
+ createdAt: z5.string(),
872
+ txHash: z5.string(),
873
+ blockNumber: z5.number().optional(),
874
+ chain: z5.string(),
875
+ payer: z5.string(),
876
+ recipient: z5.string(),
877
+ token: z5.enum(["USDC", "USDT", "RLUSD"]),
878
+ tokenAmount: z5.string(),
879
+ tokenAmountRaw: z5.string(),
880
+ method: z5.enum(["eip7702", "eip3009", "eip7702_xlayer", "eip7702_stable"]),
881
+ gasCostNative: z5.string().optional(),
882
+ apiKeyTier: z5.string().optional(),
628
883
  // hidden by default in publicView
629
- showTier: z4.boolean(),
630
- sandbox: z4.boolean(),
631
- webhook: z4.object({
632
- configured: z4.boolean(),
633
- event: z4.string(),
634
- deliveryStatus: z4.enum(["pending", "delivered", "failed", "not_configured"]),
635
- attempts: z4.number().optional(),
636
- lastStatusCode: z4.number().optional(),
637
- lastError: z4.string().optional(),
638
- deliveredAt: z4.string().optional(),
639
- payloadSha256: z4.string().optional(),
640
- signatureSha256: z4.string().optional()
884
+ showTier: z5.boolean(),
885
+ sandbox: z5.boolean(),
886
+ webhook: z5.object({
887
+ configured: z5.boolean(),
888
+ event: z5.string(),
889
+ deliveryStatus: z5.enum(["pending", "delivered", "failed", "not_configured"]),
890
+ attempts: z5.number().optional(),
891
+ lastStatusCode: z5.number().optional(),
892
+ lastError: z5.string().optional(),
893
+ deliveredAt: z5.string().optional(),
894
+ payloadSha256: z5.string().optional(),
895
+ signatureSha256: z5.string().optional()
641
896
  }),
642
- signature: z4.string(),
643
- signedBy: z4.string(),
644
- signedAt: z4.string()
897
+ signature: z5.string(),
898
+ signedBy: z5.string(),
899
+ signedAt: z5.string()
645
900
  });
646
901
  function canonicalize(fields) {
647
902
  const sorted = {};
@@ -674,9 +929,9 @@ function verifyReceiptSignature(r) {
674
929
  return false;
675
930
  }
676
931
  }
677
- var ReceiptInputSchema = z4.object({
678
- receiptId: z4.string().regex(/^rct_[0-9a-f]{24}$/, "receiptId must match rct_<24-hex>").optional(),
679
- txHash: z4.string().regex(/^0x[0-9a-fA-F]{64}$/, "txHash must be a 0x-prefixed 32-byte hex").optional()
932
+ var ReceiptInputSchema = z5.object({
933
+ receiptId: z5.string().regex(/^rct_[0-9a-f]{24}$/, "receiptId must match rct_<24-hex>").optional(),
934
+ txHash: z5.string().regex(/^0x[0-9a-fA-F]{64}$/, "txHash must be a 0x-prefixed 32-byte hex").optional()
680
935
  }).refine((v) => !!v.receiptId || !!v.txHash, {
681
936
  message: "Provide receiptId OR txHash (at least one)"
682
937
  });
@@ -773,7 +1028,7 @@ var RECEIPT_TOOL = {
773
1028
 
774
1029
  // src/index.ts
775
1030
  var PACKAGE_NAME = "@quackai/q402-mcp";
776
- var PACKAGE_VERSION = "0.3.7";
1031
+ var PACKAGE_VERSION = "0.3.9";
777
1032
  function jsonText(value) {
778
1033
  return { type: "text", text: JSON.stringify(value, null, 2) };
779
1034
  }
@@ -783,7 +1038,7 @@ async function main() {
783
1038
  { capabilities: { tools: {} } }
784
1039
  );
785
1040
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
786
- tools: [QUOTE_TOOL, BALANCE_TOOL, PAY_TOOL, RECEIPT_TOOL]
1041
+ tools: [QUOTE_TOOL, BALANCE_TOOL, PAY_TOOL, BATCH_PAY_TOOL, RECEIPT_TOOL]
787
1042
  }));
788
1043
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
789
1044
  const { name, arguments: args } = req.params;
@@ -801,6 +1056,10 @@ async function main() {
801
1056
  const parsed = PayInputSchema.parse(args ?? {});
802
1057
  return { content: [jsonText(await runPay(parsed))] };
803
1058
  }
1059
+ case "q402_batch_pay": {
1060
+ const parsed = BatchPayInputSchema.parse(args ?? {});
1061
+ return { content: [jsonText(await runBatchPay(parsed))] };
1062
+ }
804
1063
  case "q402_receipt": {
805
1064
  const parsed = ReceiptInputSchema.parse(args ?? {});
806
1065
  return { content: [jsonText(await runReceipt(parsed))] };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quackai/q402-mcp",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "MCP server for Q402 — gasless USDC, USDT, and RLUSD payments across 7 EVM chains, callable directly from Claude Desktop and any other Model Context Protocol client.",
5
5
  "mcpName": "io.github.bitgett/q402-mcp",
6
6
  "keywords": [