@quackai/q402-mcp 0.8.19 → 0.8.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +102 -16
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -130,8 +130,7 @@ function loadConfig() {
|
|
|
130
130
|
const walletId = typeof walletIdRaw === "string" && walletIdRaw.length > 0 ? walletIdRaw.toLowerCase() : null;
|
|
131
131
|
const realPaymentsRequested = ENV.Q402_ENABLE_REAL_PAYMENTS === "1";
|
|
132
132
|
const anyLiveKey = classifyApiKey(trialApiKey) === "live" || classifyApiKey(multichainApiKey) === "live" || classifyApiKey(legacyApiKey) === "live";
|
|
133
|
-
const
|
|
134
|
-
const live = realPaymentsRequested && anyLiveKey && hasAnySignerKey;
|
|
133
|
+
const live = realPaymentsRequested && anyLiveKey;
|
|
135
134
|
return {
|
|
136
135
|
trialApiKey,
|
|
137
136
|
multichainApiKey,
|
|
@@ -212,7 +211,7 @@ var isValidPrivateKey = (s) => typeof s === "string" && PRIVATE_KEY_RE.test(s);
|
|
|
212
211
|
// package.json
|
|
213
212
|
var package_default = {
|
|
214
213
|
name: "@quackai/q402-mcp",
|
|
215
|
-
version: "0.8.
|
|
214
|
+
version: "0.8.21",
|
|
216
215
|
description: "MCP server for Q402 \u2014 gasless USDC/USDT/RLUSD payments on 10 EVM chains + Chainlink CCIP USDC bridge on the eth/avax/arbitrum triangle, callable from Claude (Desktop / Code), OpenAI Codex CLI, and any other Model Context Protocol client.",
|
|
217
216
|
mcpName: "io.github.bitgett/q402-mcp",
|
|
218
217
|
keywords: [
|
|
@@ -1648,6 +1647,21 @@ async function runBatchPay(input) {
|
|
|
1648
1647
|
const data = await resp.json().catch(() => ({}));
|
|
1649
1648
|
if (!resp.ok) {
|
|
1650
1649
|
const errMsg = data && typeof data === "object" && "error" in data ? String(data.error) : `relay_http_${resp.status}`;
|
|
1650
|
+
if (data.status === "uncertain") {
|
|
1651
|
+
return {
|
|
1652
|
+
mode: "live",
|
|
1653
|
+
status: "settlement_uncertain",
|
|
1654
|
+
guardsApplied: [
|
|
1655
|
+
...guardsApplied,
|
|
1656
|
+
"wallet=agentic-server",
|
|
1657
|
+
"mode=live",
|
|
1658
|
+
`http=${resp.status}`
|
|
1659
|
+
],
|
|
1660
|
+
senderWallet,
|
|
1661
|
+
error: errMsg,
|
|
1662
|
+
setupHint: "The relay did not confirm whether these payments settled \u2014 they MAY have been sent. DO NOT retry this batch; verify the recipients' on-chain balances first. Re-sending could double-pay."
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1651
1665
|
return {
|
|
1652
1666
|
mode: "live",
|
|
1653
1667
|
status: "aborted",
|
|
@@ -3414,7 +3428,7 @@ var YieldPositionsInputSchema = z15.object({
|
|
|
3414
3428
|
});
|
|
3415
3429
|
var YIELD_POSITIONS_TOOL = {
|
|
3416
3430
|
name: "q402_yield_positions",
|
|
3417
|
-
description: "READ-ONLY \u2014 show the Agent Wallet's current Q402 Yield (Aave) lending positions. Returns each position's protocol, chain, asset, market address, balance,
|
|
3431
|
+
description: "READ-ONLY \u2014 show the Agent Wallet's current Q402 Yield (Aave) lending positions. Returns each position's protocol, chain, asset, market address, CURRENT supplied position value (the live aToken balance, in token units), and live supply APY, plus the aggregate current value in USD. Authenticated by the configured live Multichain API key \u2014 no private key required and no funds move. BNB CHAIN ONLY \u2014 Q402 Yield supports BNB Chain today. DOES NOT report principal or accrued earnings as separate numbers \u2014 the aToken balance already includes accrued interest but is not broken out, so do NOT claim a specific 'earnings/profit/interest earned' figure from this tool; report only the current position value and the APY. walletId is OPTIONAL: omit it and the server reads the owner's default Agent Wallet (resolved from the API key); pass one only when the owner holds more than one wallet. An optional chain filter is also accepted. Use this whenever the user asks 'what is my position worth?', 'what's my current yield balance / APY?', or 'what are my open lending positions?'",
|
|
3418
3432
|
inputSchema: {
|
|
3419
3433
|
type: "object",
|
|
3420
3434
|
properties: {
|
|
@@ -3483,7 +3497,7 @@ async function runYieldPositions(input) {
|
|
|
3483
3497
|
const unavailableChains = Array.isArray(data.unavailableChains) ? data.unavailableChains : [];
|
|
3484
3498
|
const hasUnavailable = data.unavailable === true || unavailableChains.length > 0;
|
|
3485
3499
|
const unavailableNote = hasUnavailable ? ` Some chains could not be read this cycle${unavailableChains.length ? ` (${unavailableChains.join(", ")})` : ""} \u2014 this snapshot may be incomplete; retry before treating a balance as zero.` : "";
|
|
3486
|
-
const summary = positions.length ? `Total
|
|
3500
|
+
const summary = positions.length ? `Total current position value: $${(data.totalSuppliedUsd ?? 0).toFixed(2)} across ${positions.length} position(s) (live aToken balance incl. accrued interest; earnings are not broken out).${unavailableNote}` : hasUnavailable ? `No positions returned, but a read failure occurred${unavailableChains.length ? ` on ${unavailableChains.join(", ")}` : ""} \u2014 this is NOT a confirmed empty position; retry.` : "No open Q402 Yield positions for this wallet.";
|
|
3487
3501
|
return {
|
|
3488
3502
|
content: [
|
|
3489
3503
|
{ type: "text", text: summary },
|
|
@@ -3491,11 +3505,23 @@ async function runYieldPositions(input) {
|
|
|
3491
3505
|
type: "text",
|
|
3492
3506
|
text: JSON.stringify({
|
|
3493
3507
|
walletId: data.walletId ?? walletId ?? null,
|
|
3508
|
+
// Surface only what's real: the current position value (live aToken
|
|
3509
|
+
// balance) + APY. `principal`/`accrued` come back null from this
|
|
3510
|
+
// Phase-0 read, so they're included ONLY when the server actually
|
|
3511
|
+
// populated them — never as a null placeholder that reads like a
|
|
3512
|
+
// real (zero) earnings figure.
|
|
3494
3513
|
positions: positions.map((p) => ({
|
|
3495
|
-
|
|
3496
|
-
|
|
3514
|
+
protocol: p.protocol,
|
|
3515
|
+
chain: p.chain,
|
|
3516
|
+
asset: p.asset,
|
|
3517
|
+
marketAddress: p.marketAddress,
|
|
3518
|
+
currentValue: p.balance,
|
|
3519
|
+
supplyApy: p.supplyApy,
|
|
3520
|
+
supplyApyPct: Math.round(p.supplyApy * 100 * 100) / 100,
|
|
3521
|
+
...p.principal != null ? { principal: p.principal } : {},
|
|
3522
|
+
...p.accrued != null ? { accrued: p.accrued } : {}
|
|
3497
3523
|
})),
|
|
3498
|
-
|
|
3524
|
+
totalCurrentValueUsd: data.totalSuppliedUsd ?? null,
|
|
3499
3525
|
asOf: data.asOf ?? null,
|
|
3500
3526
|
unavailable: hasUnavailable,
|
|
3501
3527
|
unavailableChains
|
|
@@ -3516,7 +3542,7 @@ var YieldDepositInputSchema = z16.object({
|
|
|
3516
3542
|
"Optional Agent Wallet address to supply from (max 10 per owner). Omit to use Q402_AGENT_WALLET_ADDRESS env, then the owner's default wallet (resolved server-side from the API key)."
|
|
3517
3543
|
),
|
|
3518
3544
|
idempotencyKey: z16.string().optional().describe(
|
|
3519
|
-
|
|
3545
|
+
'Optional durable idempotency key for this logical deposit. When omitted the tool generates a FRESH random key per invocation, so each call executes a distinct deposit (two intentional same-amount deposits both run, and a re-deposit after a withdrawal is NOT replayed). Pass your own STABLE key only when you want retry-safety: re-calling with the same key replays the first result instead of double-supplying. IMPORTANT: if a call returns status="uncertain" (timeout / unconfirmed broadcast), it echoes back the idempotencyKey it used \u2014 pass THAT exact value here to safely resume the same deposit instead of starting a new one.'
|
|
3520
3546
|
),
|
|
3521
3547
|
confirm: z16.boolean().optional().describe(
|
|
3522
3548
|
"MUST be true to actually supply funds. Set this only after the user has explicitly approved this exact deposit (amount, token, chain, wallet) in the conversation. When omitted or false the tool previews the action and does NOT move any funds."
|
|
@@ -3524,7 +3550,7 @@ var YieldDepositInputSchema = z16.object({
|
|
|
3524
3550
|
});
|
|
3525
3551
|
var YIELD_DEPOSIT_TOOL = {
|
|
3526
3552
|
name: "q402_yield_deposit",
|
|
3527
|
-
description: "WRITE \u2014 MOVES FUNDS. Supplies the Agent Wallet's stablecoin (USDC / USDT) into Aave V3 (Q402 Yield) so it starts earning supply APY. Server-managed Agent Wallet path (Mode C): authenticated by the configured live Multichain API key \u2014 the server holds the encrypted key, signs the Aave supply, and sponsors gas. BNB CHAIN ONLY \u2014 Q402 Yield supports BNB Chain today; ETH / AVAX and other chains are not yet available. \n\nREQUIRES CONFIRMATION \u2014 like q402_pay, this tool refuses to execute unless `confirm: true` is set. Call it FIRST without confirm to get a one-line preview of exactly what will happen (amount, token, chain, wallet); show that to the user, get explicit approval, THEN re-call with confirm:true. Never set confirm:true on the user's behalf without that approval. \n\nSANDBOX BY DEFAULT \u2014 like q402_pay, no funds move unless a live Multichain key (q402_live_*) is configured AND Q402_ENABLE_REAL_PAYMENTS=1. Without both, confirm:true returns a sandbox preview (no on-chain supply) with a setup hint \u2014 confirm:true alone does NOT move real funds. \n\nUse q402_yield_reserves first to show available markets + APY, and q402_yield_positions afterward to confirm the supplied balance.",
|
|
3553
|
+
description: "WRITE \u2014 MOVES FUNDS. Supplies the Agent Wallet's stablecoin (USDC / USDT) into Aave V3 (Q402 Yield) so it starts earning supply APY. Server-managed Agent Wallet path (Mode C): authenticated by the configured live Multichain API key \u2014 the server holds the encrypted key, signs the Aave supply, and sponsors gas. BNB CHAIN ONLY \u2014 Q402 Yield supports BNB Chain today; ETH / AVAX and other chains are not yet available. \n\nREQUIRES CONFIRMATION \u2014 like q402_pay, this tool refuses to execute unless `confirm: true` is set. Call it FIRST without confirm to get a one-line preview of exactly what will happen (amount, token, chain, wallet); show that to the user, get explicit approval, THEN re-call with confirm:true. Never set confirm:true on the user's behalf without that approval. \n\nSANDBOX BY DEFAULT \u2014 like q402_pay, no funds move unless a live Multichain key (q402_live_*) is configured AND Q402_ENABLE_REAL_PAYMENTS=1. Without both, confirm:true returns a sandbox preview (no on-chain supply) with a setup hint \u2014 confirm:true alone does NOT move real funds. \n\nRETRY SAFETY \u2014 on a timeout or an unconfirmed broadcast the tool returns status=\"uncertain\" and echoes back the idempotencyKey it used. The deposit MAY have settled, so do NOT blindly call again \u2014 that starts a NEW deposit and can double-supply. To resume the SAME operation, re-call with idempotencyKey set to the echoed value; the server dedupes on it and replays the original result. \n\nUse q402_yield_reserves first to show available markets + APY, and q402_yield_positions afterward to confirm the supplied balance.",
|
|
3528
3554
|
inputSchema: {
|
|
3529
3555
|
type: "object",
|
|
3530
3556
|
properties: {
|
|
@@ -3548,7 +3574,7 @@ var YIELD_DEPOSIT_TOOL = {
|
|
|
3548
3574
|
},
|
|
3549
3575
|
idempotencyKey: {
|
|
3550
3576
|
type: "string",
|
|
3551
|
-
description:
|
|
3577
|
+
description: 'Optional durable idempotency key. Omit and the tool generates a FRESH random key per invocation, so every call executes a distinct deposit. Pass your own STABLE key only for opt-in retry-safety \u2014 re-calling with the same key replays the first result instead of double-supplying. If a call returns status="uncertain", it echoes the idempotencyKey it used \u2014 pass that exact value back here to resume the same deposit rather than start a new one.'
|
|
3552
3578
|
},
|
|
3553
3579
|
confirm: {
|
|
3554
3580
|
type: "boolean",
|
|
@@ -3631,13 +3657,43 @@ async function runYieldDeposit(input) {
|
|
|
3631
3657
|
return {
|
|
3632
3658
|
content: [{
|
|
3633
3659
|
type: "text",
|
|
3634
|
-
text:
|
|
3660
|
+
text: JSON.stringify({
|
|
3661
|
+
status: "uncertain",
|
|
3662
|
+
success: false,
|
|
3663
|
+
action: "yield_deposit",
|
|
3664
|
+
chain: input.chain,
|
|
3665
|
+
token: input.token,
|
|
3666
|
+
amount: input.amount,
|
|
3667
|
+
idempotencyKey,
|
|
3668
|
+
error: e instanceof Error ? e.message : String(e),
|
|
3669
|
+
message: `Network error before a confirmed response \u2014 the deposit may or may not have been submitted. Do NOT start a new deposit. To safely resume THIS operation, retry with idempotencyKey="${idempotencyKey}" (the server dedupes on it, so a retry that already landed replays the original result instead of double-supplying).`
|
|
3670
|
+
}, null, 2)
|
|
3635
3671
|
}],
|
|
3636
3672
|
isError: true
|
|
3637
3673
|
};
|
|
3638
3674
|
}
|
|
3639
3675
|
const data = await res.json().catch(() => ({}));
|
|
3640
3676
|
if (!res.ok) {
|
|
3677
|
+
if (res.status === 502) {
|
|
3678
|
+
return {
|
|
3679
|
+
content: [{
|
|
3680
|
+
type: "text",
|
|
3681
|
+
text: JSON.stringify({
|
|
3682
|
+
status: "uncertain",
|
|
3683
|
+
success: false,
|
|
3684
|
+
action: "yield_deposit",
|
|
3685
|
+
chain: input.chain,
|
|
3686
|
+
token: input.token,
|
|
3687
|
+
amount: input.amount,
|
|
3688
|
+
idempotencyKey,
|
|
3689
|
+
txHash: data.txHash ?? null,
|
|
3690
|
+
error: data.error ?? "settlement_uncertain",
|
|
3691
|
+
message: `Broadcast but unconfirmed \u2014 the deposit may have settled on-chain. Do NOT blindly start a new deposit. Verify on-chain first; if you must retry, reuse idempotencyKey="${idempotencyKey}" so the server resumes THIS operation (replays the original result) instead of supplying again.`
|
|
3692
|
+
}, null, 2)
|
|
3693
|
+
}],
|
|
3694
|
+
isError: true
|
|
3695
|
+
};
|
|
3696
|
+
}
|
|
3641
3697
|
return {
|
|
3642
3698
|
content: [{
|
|
3643
3699
|
type: "text",
|
|
@@ -3666,7 +3722,7 @@ var YieldWithdrawInputSchema = z17.object({
|
|
|
3666
3722
|
"Optional Agent Wallet address to withdraw to (max 10 per owner). Omit to use Q402_AGENT_WALLET_ADDRESS env, then the owner's default wallet (resolved server-side from the API key)."
|
|
3667
3723
|
),
|
|
3668
3724
|
idempotencyKey: z17.string().optional().describe(
|
|
3669
|
-
|
|
3725
|
+
'Optional durable idempotency key for this logical withdrawal. When omitted the tool generates a FRESH random key per invocation, so each call executes a distinct withdrawal (a re-deposit followed by another `withdraw max` is NOT replayed). Pass your own STABLE key only when you want retry-safety: re-calling with the same key replays the first result instead of double-withdrawing. IMPORTANT: if a call returns status="uncertain" (timeout / unconfirmed broadcast), it echoes back the idempotencyKey it used \u2014 pass THAT exact value here to safely resume the same withdrawal instead of starting a new one.'
|
|
3670
3726
|
),
|
|
3671
3727
|
confirm: z17.boolean().optional().describe(
|
|
3672
3728
|
"MUST be true to actually withdraw funds. Set this only after the user has explicitly approved this exact withdrawal (amount, token, chain, wallet) in the conversation. When omitted or false the tool previews the action and does NOT move any funds."
|
|
@@ -3674,7 +3730,7 @@ var YieldWithdrawInputSchema = z17.object({
|
|
|
3674
3730
|
});
|
|
3675
3731
|
var YIELD_WITHDRAW_TOOL = {
|
|
3676
3732
|
name: "q402_yield_withdraw",
|
|
3677
|
-
description: 'WRITE \u2014 MOVES FUNDS. Withdraws the Agent Wallet\'s supplied stablecoin (USDC / USDT) out of Aave V3 (Q402 Yield) back to the Agent Wallet. Pass amount="max" to withdraw the FULL position. Server-managed Agent Wallet path (Mode C): authenticated by the configured live Multichain API key \u2014 the server holds the encrypted key, signs the Aave withdraw, and sponsors gas. BNB CHAIN ONLY \u2014 Q402 Yield supports BNB Chain today; ETH / AVAX and other chains are not yet available. \n\nREQUIRES CONFIRMATION \u2014 like q402_pay, this tool refuses to execute unless `confirm: true` is set. Call it FIRST without confirm to get a one-line preview of exactly what will happen (amount, token, chain, wallet); show that to the user, get explicit approval, THEN re-call with confirm:true. Never set confirm:true on the user\'s behalf without that approval. \n\nSANDBOX BY DEFAULT \u2014 like q402_pay, no funds move unless a live Multichain key (q402_live_*) is configured AND Q402_ENABLE_REAL_PAYMENTS=1. Without both, confirm:true returns a sandbox preview (no on-chain withdraw) with a setup hint \u2014 confirm:true alone does NOT move real funds. \n\nUse q402_yield_positions first to see the current position size (especially before an amount="max" withdrawal).',
|
|
3733
|
+
description: 'WRITE \u2014 MOVES FUNDS. Withdraws the Agent Wallet\'s supplied stablecoin (USDC / USDT) out of Aave V3 (Q402 Yield) back to the Agent Wallet. Pass amount="max" to withdraw the FULL position. Server-managed Agent Wallet path (Mode C): authenticated by the configured live Multichain API key \u2014 the server holds the encrypted key, signs the Aave withdraw, and sponsors gas. BNB CHAIN ONLY \u2014 Q402 Yield supports BNB Chain today; ETH / AVAX and other chains are not yet available. \n\nREQUIRES CONFIRMATION \u2014 like q402_pay, this tool refuses to execute unless `confirm: true` is set. Call it FIRST without confirm to get a one-line preview of exactly what will happen (amount, token, chain, wallet); show that to the user, get explicit approval, THEN re-call with confirm:true. Never set confirm:true on the user\'s behalf without that approval. \n\nSANDBOX BY DEFAULT \u2014 like q402_pay, no funds move unless a live Multichain key (q402_live_*) is configured AND Q402_ENABLE_REAL_PAYMENTS=1. Without both, confirm:true returns a sandbox preview (no on-chain withdraw) with a setup hint \u2014 confirm:true alone does NOT move real funds. \n\nRETRY SAFETY \u2014 on a timeout or an unconfirmed broadcast the tool returns status="uncertain" and echoes back the idempotencyKey it used. The withdrawal MAY have settled, so do NOT blindly call again \u2014 that starts a NEW withdrawal and can double-withdraw. To resume the SAME operation, re-call with idempotencyKey set to the echoed value; the server dedupes on it and replays the original result. \n\nUse q402_yield_positions first to see the current position size (especially before an amount="max" withdrawal).',
|
|
3678
3734
|
inputSchema: {
|
|
3679
3735
|
type: "object",
|
|
3680
3736
|
properties: {
|
|
@@ -3698,7 +3754,7 @@ var YIELD_WITHDRAW_TOOL = {
|
|
|
3698
3754
|
},
|
|
3699
3755
|
idempotencyKey: {
|
|
3700
3756
|
type: "string",
|
|
3701
|
-
description:
|
|
3757
|
+
description: 'Optional durable idempotency key. Omit and the tool generates a FRESH random key per invocation, so every call executes a distinct withdrawal. Pass your own STABLE key only for opt-in retry-safety \u2014 re-calling with the same key replays the first result instead of double-withdrawing. If a call returns status="uncertain", it echoes the idempotencyKey it used \u2014 pass that exact value back here to resume the same withdrawal rather than start a new one.'
|
|
3702
3758
|
},
|
|
3703
3759
|
confirm: {
|
|
3704
3760
|
type: "boolean",
|
|
@@ -3782,13 +3838,43 @@ async function runYieldWithdraw(input) {
|
|
|
3782
3838
|
return {
|
|
3783
3839
|
content: [{
|
|
3784
3840
|
type: "text",
|
|
3785
|
-
text:
|
|
3841
|
+
text: JSON.stringify({
|
|
3842
|
+
status: "uncertain",
|
|
3843
|
+
success: false,
|
|
3844
|
+
action: "yield_withdraw",
|
|
3845
|
+
chain: input.chain,
|
|
3846
|
+
token: input.token,
|
|
3847
|
+
amount: input.amount,
|
|
3848
|
+
idempotencyKey,
|
|
3849
|
+
error: e instanceof Error ? e.message : String(e),
|
|
3850
|
+
message: `Network error before a confirmed response \u2014 the withdrawal may or may not have been submitted. Do NOT start a new withdrawal. To safely resume THIS operation, retry with idempotencyKey="${idempotencyKey}" (the server dedupes on it, so a retry that already landed replays the original result instead of double-withdrawing).`
|
|
3851
|
+
}, null, 2)
|
|
3786
3852
|
}],
|
|
3787
3853
|
isError: true
|
|
3788
3854
|
};
|
|
3789
3855
|
}
|
|
3790
3856
|
const data = await res.json().catch(() => ({}));
|
|
3791
3857
|
if (!res.ok) {
|
|
3858
|
+
if (res.status === 502) {
|
|
3859
|
+
return {
|
|
3860
|
+
content: [{
|
|
3861
|
+
type: "text",
|
|
3862
|
+
text: JSON.stringify({
|
|
3863
|
+
status: "uncertain",
|
|
3864
|
+
success: false,
|
|
3865
|
+
action: "yield_withdraw",
|
|
3866
|
+
chain: input.chain,
|
|
3867
|
+
token: input.token,
|
|
3868
|
+
amount: input.amount,
|
|
3869
|
+
idempotencyKey,
|
|
3870
|
+
txHash: data.txHash ?? null,
|
|
3871
|
+
error: data.error ?? "settlement_uncertain",
|
|
3872
|
+
message: `Broadcast but unconfirmed \u2014 the withdrawal may have settled on-chain. Do NOT blindly start a new withdrawal. Verify on-chain first; if you must retry, reuse idempotencyKey="${idempotencyKey}" so the server resumes THIS operation (replays the original result) instead of withdrawing again.`
|
|
3873
|
+
}, null, 2)
|
|
3874
|
+
}],
|
|
3875
|
+
isError: true
|
|
3876
|
+
};
|
|
3877
|
+
}
|
|
3792
3878
|
return {
|
|
3793
3879
|
content: [{
|
|
3794
3880
|
type: "text",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quackai/q402-mcp",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.21",
|
|
4
4
|
"description": "MCP server for Q402 — gasless USDC/USDT/RLUSD payments on 10 EVM chains + Chainlink CCIP USDC bridge on the eth/avax/arbitrum triangle, callable from Claude (Desktop / Code), OpenAI Codex CLI, and any other Model Context Protocol client.",
|
|
5
5
|
"mcpName": "io.github.bitgett/q402-mcp",
|
|
6
6
|
"keywords": [
|