@quackai/q402-mcp 0.5.5 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -71
- package/dist/index.js +395 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,95 +15,89 @@ AI agents — Claude (Desktop / Code), OpenAI Codex CLI, Cursor, Cline, and any
|
|
|
15
15
|
|
|
16
16
|
## Quick start
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Two steps:
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
1. Register the MCP server with your client (one-line install per client).
|
|
21
|
+
2. Open your client and say: **"Set up Q402"**. The agent calls `q402_doctor` which walks you through creating a single secrets file at `~/.q402/mcp.env` and pasting in your API key + private key. Nothing else.
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
claude mcp add q402 -- npx -y @quackai/q402-mcp
|
|
24
|
-
```
|
|
23
|
+
### 1. Register the server
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"args": ["-y", "@quackai/q402-mcp"]
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
```
|
|
25
|
+
| Client | Command / config |
|
|
26
|
+
|---|---|
|
|
27
|
+
| **Claude Desktop / Claude Code** | `claude mcp add q402 -- npx -y @quackai/q402-mcp` |
|
|
28
|
+
| **OpenAI Codex CLI** | `codex mcp add q402 -- npx -y @quackai/q402-mcp` |
|
|
29
|
+
| **Cursor** | Add to `~/.cursor/mcp.json`: `{ "mcpServers": { "q402": { "command": "npx", "args": ["-y", "@quackai/q402-mcp"] } } }` |
|
|
30
|
+
| **Cline** | Cline → Settings → MCP Servers → Edit JSON. Same shape as Cursor. |
|
|
31
|
+
| **Any other stdio MCP client** | Point it at `npx -y @quackai/q402-mcp`. No client-specific code. |
|
|
38
32
|
|
|
39
|
-
|
|
33
|
+
That's it — secrets are NOT configured here. The MCP server reads them from `~/.q402/mcp.env` at startup (same pattern as AWS CLI / Stripe CLI / gh CLI), so every client uses the same file with no per-client wiring.
|
|
40
34
|
|
|
41
|
-
|
|
35
|
+
### 2. First-time setup
|
|
42
36
|
|
|
43
|
-
|
|
37
|
+
Restart your client, then ask your agent:
|
|
44
38
|
|
|
45
|
-
|
|
39
|
+
> *"Set up Q402"*
|
|
46
40
|
|
|
47
|
-
|
|
41
|
+
The agent calls `q402_doctor`. On first install, the tool tells the agent to:
|
|
48
42
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
43
|
+
1. Offer to create `~/.q402/mcp.env` (placeholder values only)
|
|
44
|
+
2. Open the file in your editor (`code` / `open` / `start` / `xdg-open`)
|
|
45
|
+
3. Walk you through pasting in (a) your API key from <https://q402.quackai.ai/event> (free Trial) or <https://q402.quackai.ai/payment> (paid Multichain), and (b) your wallet private key — **into the file, not into chat**
|
|
46
|
+
4. Restart the client and run `q402_doctor` again to verify
|
|
47
|
+
|
|
48
|
+
🔒 **Q402 never asks you to paste your private key into chat.** The MCP server signs payments LOCALLY on your machine — your key never leaves your device.
|
|
53
49
|
|
|
54
|
-
|
|
50
|
+
### Manual setup (no AI)
|
|
55
51
|
|
|
56
|
-
|
|
52
|
+
Create `~/.q402/mcp.env` yourself:
|
|
57
53
|
|
|
58
54
|
```bash
|
|
59
|
-
|
|
55
|
+
# ~/.q402/mcp.env
|
|
56
|
+
# Pick ONE of these:
|
|
57
|
+
Q402_TRIAL_API_KEY=q402_live_...
|
|
58
|
+
# Q402_MULTICHAIN_API_KEY=q402_live_...
|
|
59
|
+
|
|
60
|
+
Q402_PRIVATE_KEY=0x...
|
|
61
|
+
Q402_ENABLE_REAL_PAYMENTS=1
|
|
62
|
+
Q402_RELAY_BASE_URL=https://q402.quackai.ai/api
|
|
63
|
+
|
|
64
|
+
# Optional safety guards:
|
|
65
|
+
# Q402_MAX_AMOUNT_PER_CALL=5
|
|
66
|
+
# Q402_ALLOWED_RECIPIENTS=0xabc...,0xdef...
|
|
60
67
|
```
|
|
61
68
|
|
|
62
|
-
|
|
69
|
+
Then `chmod 600 ~/.q402/mcp.env` (Unix) and restart your client. That's the full configuration.
|
|
63
70
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
args = ["-y", "@quackai/q402-mcp"]
|
|
68
|
-
startup_timeout_sec = 20.0
|
|
69
|
-
```
|
|
71
|
+
### Advanced — explicit env injection
|
|
72
|
+
|
|
73
|
+
If you'd rather skip the file and inject env vars yourself (e.g. via Codex `env_vars` allow-list, a secrets manager, or shell exports), the server falls through to `process.env` — and `process.env` wins over file values on conflicts. So existing shell-export setups keep working unchanged.
|
|
70
74
|
|
|
71
|
-
|
|
75
|
+
<details>
|
|
76
|
+
<summary>Codex <code>env_vars</code> allow-list example</summary>
|
|
72
77
|
|
|
73
78
|
```toml
|
|
74
79
|
[mcp_servers.q402]
|
|
75
80
|
command = "npx"
|
|
76
81
|
args = ["-y", "@quackai/q402-mcp"]
|
|
77
82
|
startup_timeout_sec = 20.0
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
# can ask the user — pass keyScope="trial" (first 5), "multichain" (all
|
|
86
|
-
# paid), or call twice (5 free + remainder paid).
|
|
87
|
-
# Both keys use the same q402_live_ prefix — the env var name is what
|
|
88
|
-
# carries the scope, not the key string. Get the values from the
|
|
89
|
-
# dashboard (each key has its own copy button per view).
|
|
90
|
-
Q402_TRIAL_API_KEY = "q402_live_...", # BNB-only sponsored (from /event)
|
|
91
|
-
Q402_MULTICHAIN_API_KEY = "q402_live_...", # paid 9-chain (from /payment)
|
|
92
|
-
# Legacy fallback — used if neither scoped key above is set.
|
|
93
|
-
Q402_API_KEY = "q402_live_...",
|
|
94
|
-
Q402_PRIVATE_KEY = "0xabc...",
|
|
95
|
-
Q402_ENABLE_REAL_PAYMENTS = "1",
|
|
96
|
-
Q402_MAX_AMOUNT_PER_CALL = "5",
|
|
97
|
-
}
|
|
83
|
+
env_vars = [
|
|
84
|
+
"Q402_TRIAL_API_KEY",
|
|
85
|
+
"Q402_MULTICHAIN_API_KEY",
|
|
86
|
+
"Q402_PRIVATE_KEY",
|
|
87
|
+
"Q402_ENABLE_REAL_PAYMENTS",
|
|
88
|
+
"Q402_RELAY_BASE_URL",
|
|
89
|
+
]
|
|
98
90
|
```
|
|
99
91
|
|
|
100
|
-
|
|
92
|
+
Then export the values in `~/.zshrc` / `~/.bashrc`. See the [Codex config reference](https://developers.openai.com/codex/config-reference) for the full schema.
|
|
101
93
|
|
|
102
|
-
|
|
94
|
+
</details>
|
|
103
95
|
|
|
104
|
-
###
|
|
96
|
+
### Try it without any setup
|
|
105
97
|
|
|
106
|
-
|
|
98
|
+
`q402_quote` works with zero configuration — no API key, no private key, no env file. Ask:
|
|
99
|
+
|
|
100
|
+
> *"Compare gas costs to send 50 USDC to vitalik.eth across all 9 Q402 chains."*
|
|
107
101
|
|
|
108
102
|
---
|
|
109
103
|
|
|
@@ -115,6 +109,7 @@ The server has no client-specific code. If your client speaks stdio MCP, point i
|
|
|
115
109
|
|
|
116
110
|
| Tool | Auth | Purpose |
|
|
117
111
|
|---|---|---|
|
|
112
|
+
| `q402_doctor` | none | Health check covering BOTH first-install onboarding AND ongoing operational diagnostics. AI calls this when the user says "set up Q402" / "verify Q402" / "why isn't Q402 working". On first install, returns a `recommendedActions[]` payload telling the client to create `~/.q402/mcp.env` and open it in the user's editor. Later phases verify per-scope quota, EIP-7702 delegation state per chain, relay reachability, and surface slot-mismatch warnings (e.g. Trial-tier key sitting in `Q402_MULTICHAIN_API_KEY` would silently burn paid quota). Read-only — no signing, no on-chain action. |
|
|
118
113
|
| `q402_quote` | none | Compare gas cost and supported tokens across chains. Read-only. |
|
|
119
114
|
| `q402_balance` | API key | Verify the API key and report its plan tier + remaining quota credits (live vs sandbox). |
|
|
120
115
|
| `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). |
|
|
@@ -136,12 +131,12 @@ The server has no client-specific code. If your client speaks stdio MCP, point i
|
|
|
136
131
|
|
|
137
132
|
## Sandbox vs live mode
|
|
138
133
|
|
|
139
|
-
By default the MCP server operates in **sandbox mode**: `q402_pay` returns a random fake transaction hash (32-byte hex, no on-chain broadcast), no funds move, no gas-tank credit is consumed. That makes it safe to plug into any MCP client without worrying about an
|
|
134
|
+
By default the MCP server operates in **sandbox mode**: `q402_pay` returns a random fake transaction hash (32-byte hex, no on-chain broadcast), no funds move, no gas-tank credit is consumed. That makes it safe to plug into any MCP client without worrying about an accidental payment — if the agent misreads the conversation and fires `q402_pay` before you intended, nothing moves.
|
|
140
135
|
|
|
141
136
|
To enable real on-chain transactions, the resolved API key must be live (`q402_live_*`), `Q402_PRIVATE_KEY` must be set, and `Q402_ENABLE_REAL_PAYMENTS=1`:
|
|
142
137
|
|
|
143
138
|
```bash
|
|
144
|
-
# Two-key model — set whichever applies
|
|
139
|
+
# Two-key model — set whichever applies (or both for auto-routing).
|
|
145
140
|
# Auto-routing (same for q402_pay AND q402_batch_pay):
|
|
146
141
|
# chain="bnb" + Q402_TRIAL_API_KEY set → Trial (free sponsored)
|
|
147
142
|
# anything else → Multichain (paid 9-chain)
|
|
@@ -151,10 +146,6 @@ To enable real on-chain transactions, the resolved API key must be live (`q402_l
|
|
|
151
146
|
Q402_TRIAL_API_KEY=q402_live_... # BNB-only sponsored Trial key (from /event)
|
|
152
147
|
Q402_MULTICHAIN_API_KEY=q402_live_... # paid 9-chain key (per-chain Gas Tank)
|
|
153
148
|
|
|
154
|
-
# Legacy fallback. Used for both scopes when the two above are unset —
|
|
155
|
-
# single-env setups (only Q402_API_KEY set) keep working unchanged.
|
|
156
|
-
Q402_API_KEY=q402_live_...
|
|
157
|
-
|
|
158
149
|
Q402_PRIVATE_KEY=0xabc... # signer for the payer EOA
|
|
159
150
|
Q402_ENABLE_REAL_PAYMENTS=1 # explicit opt-in
|
|
160
151
|
```
|
|
@@ -184,13 +175,14 @@ Combined with the `confirm: true` argument the tool requires, this means the mod
|
|
|
184
175
|
|---|---|---|
|
|
185
176
|
| `Q402_TRIAL_API_KEY` | live-pay (BNB) | BNB-only sponsored Trial key. Free at https://q402.quackai.ai/event. Auto-routed for `chain="bnb"` in both `q402_pay` and `q402_batch_pay` (≤5 recipients) when set. 6+ recipient BNB batches return `status="ambiguous"` so the agent can ask the user how to split. |
|
|
186
177
|
| `Q402_MULTICHAIN_API_KEY` | live-pay (9-chain) | Paid 9-chain key. Get one at https://q402.quackai.ai/payment. Auto-routed for non-BNB chains AND for BNB when no Trial key is set. Cap: 20 recipients per batch. |
|
|
187
|
-
| `Q402_API_KEY` | legacy fallback | Single-env legacy path. Used for both scopes when the two above are unset. Keep set if you only have one key. |
|
|
188
178
|
| `Q402_PRIVATE_KEY` | live-pay | Signer for the payer EOA. **Never share. Never paste in chat.** |
|
|
189
179
|
| `Q402_ENABLE_REAL_PAYMENTS` | live-pay | Set to `1` to opt in. Any other value (or unset) → sandbox. |
|
|
190
180
|
| `Q402_MAX_AMOUNT_PER_CALL` | optional | USD-equivalent cap. Defaults to `5`. |
|
|
191
181
|
| `Q402_ALLOWED_RECIPIENTS` | optional | Comma-separated lowercase addresses. Defaults to no allowlist. |
|
|
192
182
|
| `Q402_RELAY_BASE_URL` | optional | Defaults to `https://q402.quackai.ai/api`. Override for self-hosted Q402. |
|
|
193
183
|
|
|
184
|
+
> Older integrations may still set `Q402_API_KEY` as a single-env fallback — that still works silently for back-compat. New setups should use the two-key model above; `q402_doctor` only guides users to those two.
|
|
185
|
+
|
|
194
186
|
---
|
|
195
187
|
|
|
196
188
|
## Supported chains
|
|
@@ -211,11 +203,11 @@ Combined with the `confirm: true` argument the tool requires, this means the mod
|
|
|
211
203
|
|
|
212
204
|
## Why this exists
|
|
213
205
|
|
|
214
|
-
|
|
206
|
+
AI agents are becoming the default interface for software, but the moment they need to move money the stack breaks: holding gas tokens, signing every transaction, managing wallets across many chains. None of that scales when the agent is supposed to act on its own.
|
|
215
207
|
|
|
216
|
-
Q402
|
|
208
|
+
Q402 is the payment layer for that gap. A single signing primitive (EIP-712 + EIP-7702) settles gasless stablecoin payments across 9 EVM chains, with an ECDSA-signed Trust Receipt for every transaction. The MCP package exposes that surface inside Claude, Codex, Cursor, and Cline — your agent can quote, send, batch, and audit payments from a natural-language prompt.
|
|
217
209
|
|
|
218
|
-
|
|
210
|
+
Single transfers and multi-recipient batches ship today. The next layer — recurring payouts, conditional execution, and policy-gated treasury automation — is the same primitive composed differently. We're building toward agents that operate real budgets, settle among themselves, and move value through workflows no human triggers manually.
|
|
219
211
|
|
|
220
212
|
---
|
|
221
213
|
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,73 @@ import {
|
|
|
9
9
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
10
|
|
|
11
11
|
// src/config.ts
|
|
12
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
13
|
+
import { homedir } from "os";
|
|
14
|
+
import { join } from "path";
|
|
12
15
|
import { isAddress } from "ethers";
|
|
16
|
+
var Q402_ENV_FILE = join(homedir(), ".q402", "mcp.env");
|
|
17
|
+
function loadQ402EnvFileFromPath(path) {
|
|
18
|
+
if (!existsSync(path)) return {};
|
|
19
|
+
if (process.platform !== "win32") {
|
|
20
|
+
try {
|
|
21
|
+
const mode = statSync(path).mode & 511;
|
|
22
|
+
if (mode & 63) {
|
|
23
|
+
process.stderr.write(
|
|
24
|
+
`[q402-mcp] warning: ${path} is readable by group/other (mode ${mode.toString(8)}). Run: chmod 600 ${path}
|
|
25
|
+
`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const out = {};
|
|
32
|
+
let raw;
|
|
33
|
+
try {
|
|
34
|
+
raw = readFileSync(path, "utf-8");
|
|
35
|
+
} catch (e) {
|
|
36
|
+
process.stderr.write(
|
|
37
|
+
`[q402-mcp] warning: could not read ${path}: ${e instanceof Error ? e.message : String(e)}
|
|
38
|
+
`
|
|
39
|
+
);
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
43
|
+
const t = line.trim();
|
|
44
|
+
if (!t || t.startsWith("#")) continue;
|
|
45
|
+
const eq = t.indexOf("=");
|
|
46
|
+
if (eq < 0) continue;
|
|
47
|
+
const k = t.slice(0, eq).trim();
|
|
48
|
+
if (!k.startsWith("Q402_")) continue;
|
|
49
|
+
let rawVal = t.slice(eq + 1).trim();
|
|
50
|
+
const quoted = /^(['"])(.*)\1\s*(?:#.*)?$/.exec(rawVal);
|
|
51
|
+
if (quoted) {
|
|
52
|
+
rawVal = quoted[2];
|
|
53
|
+
} else {
|
|
54
|
+
const hashIdx = rawVal.search(/\s#/);
|
|
55
|
+
if (hashIdx >= 0) rawVal = rawVal.slice(0, hashIdx).trimEnd();
|
|
56
|
+
}
|
|
57
|
+
out[k] = rawVal;
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
function loadQ402EnvFile() {
|
|
62
|
+
return loadQ402EnvFileFromPath(Q402_ENV_FILE);
|
|
63
|
+
}
|
|
64
|
+
var FILE_ENV = loadQ402EnvFile();
|
|
65
|
+
var ENV = Object.freeze({
|
|
66
|
+
...FILE_ENV,
|
|
67
|
+
...process.env
|
|
68
|
+
});
|
|
69
|
+
var Q402_ENV_FILE_PATH = Q402_ENV_FILE;
|
|
70
|
+
var Q402_ENV_FILE_PRESENT = existsSync(Q402_ENV_FILE);
|
|
71
|
+
var Q402_ENV_FILE_KEYS = Object.freeze(
|
|
72
|
+
new Set(
|
|
73
|
+
Object.keys(FILE_ENV).filter((k) => process.env[k] === void 0)
|
|
74
|
+
)
|
|
75
|
+
);
|
|
76
|
+
var Q402_ENV_FILE_KEYS_ALL = Object.freeze(
|
|
77
|
+
new Set(Object.keys(FILE_ENV))
|
|
78
|
+
);
|
|
13
79
|
var DEFAULT_RELAY_BASE = "https://q402.quackai.ai/api";
|
|
14
80
|
var DEFAULT_MAX_AMOUNT = 5;
|
|
15
81
|
function classifyApiKey(k) {
|
|
@@ -29,13 +95,13 @@ function parseMaxAmount(raw) {
|
|
|
29
95
|
return n;
|
|
30
96
|
}
|
|
31
97
|
function loadConfig() {
|
|
32
|
-
const trialApiKey =
|
|
33
|
-
const multichainApiKey =
|
|
34
|
-
const legacyApiKey =
|
|
98
|
+
const trialApiKey = ENV.Q402_TRIAL_API_KEY ?? null;
|
|
99
|
+
const multichainApiKey = ENV.Q402_MULTICHAIN_API_KEY ?? null;
|
|
100
|
+
const legacyApiKey = ENV.Q402_API_KEY ?? null;
|
|
35
101
|
const apiKey = multichainApiKey ?? trialApiKey ?? legacyApiKey;
|
|
36
102
|
const apiKeyKind = classifyApiKey(apiKey);
|
|
37
|
-
const privateKey =
|
|
38
|
-
const realPaymentsRequested =
|
|
103
|
+
const privateKey = ENV.Q402_PRIVATE_KEY ?? null;
|
|
104
|
+
const realPaymentsRequested = ENV.Q402_ENABLE_REAL_PAYMENTS === "1";
|
|
39
105
|
const live = realPaymentsRequested && apiKeyKind === "live" && typeof privateKey === "string" && privateKey.length > 0;
|
|
40
106
|
return {
|
|
41
107
|
trialApiKey,
|
|
@@ -46,9 +112,9 @@ function loadConfig() {
|
|
|
46
112
|
privateKey,
|
|
47
113
|
realPaymentsRequested,
|
|
48
114
|
mode: live ? "live" : "sandbox",
|
|
49
|
-
relayBaseUrl: (
|
|
50
|
-
maxAmountPerCallUsd: parseMaxAmount(
|
|
51
|
-
allowedRecipients: parseAllowedRecipients(
|
|
115
|
+
relayBaseUrl: (ENV.Q402_RELAY_BASE_URL ?? DEFAULT_RELAY_BASE).replace(/\/$/, ""),
|
|
116
|
+
maxAmountPerCallUsd: parseMaxAmount(ENV.Q402_MAX_AMOUNT_PER_CALL),
|
|
117
|
+
allowedRecipients: parseAllowedRecipients(ENV.Q402_ALLOWED_RECIPIENTS)
|
|
52
118
|
};
|
|
53
119
|
}
|
|
54
120
|
var CONFIG = loadConfig();
|
|
@@ -90,12 +156,19 @@ function resolveApiKey(chain, scope = "auto") {
|
|
|
90
156
|
}
|
|
91
157
|
return { apiKey: key, scope: "multichain", fromLegacyFallback: !CONFIG.multichainApiKey };
|
|
92
158
|
}
|
|
159
|
+
var PRIVATE_KEY_RE = /^0x[a-fA-F0-9]{64}$/;
|
|
93
160
|
function isLiveModeFor(resolved) {
|
|
94
161
|
if (!resolved.apiKey) return false;
|
|
95
162
|
if (!CONFIG.realPaymentsRequested) return false;
|
|
96
163
|
if (!CONFIG.privateKey) return false;
|
|
164
|
+
if (!PRIVATE_KEY_RE.test(CONFIG.privateKey)) return false;
|
|
97
165
|
return resolved.apiKey.startsWith("q402_live_");
|
|
98
166
|
}
|
|
167
|
+
var isValidPrivateKey = (s) => typeof s === "string" && PRIVATE_KEY_RE.test(s);
|
|
168
|
+
|
|
169
|
+
// src/version.ts
|
|
170
|
+
var PACKAGE_NAME = "@quackai/q402-mcp";
|
|
171
|
+
var PACKAGE_VERSION = "0.5.7";
|
|
99
172
|
|
|
100
173
|
// src/tools/quote.ts
|
|
101
174
|
import { z } from "zod";
|
|
@@ -773,7 +846,7 @@ function describeSandboxReason(resolvedKey, scope) {
|
|
|
773
846
|
if (missing.length === 0) return "Sandbox mode active (no env state change needed).";
|
|
774
847
|
const tier = scope === "trial" ? "Free Trial" : "Multichain";
|
|
775
848
|
const url = scope === "trial" ? "https://q402.quackai.ai/event" : "https://q402.quackai.ai/payment";
|
|
776
|
-
return "Sandbox mode is active because the following env vars are missing or not yet set: " + missing.join(", ") + `. Get a live ${tier} key at ${url}.`;
|
|
849
|
+
return "Sandbox mode is active because the following env vars are missing or not yet set: " + missing.join(", ") + `. Get a live ${tier} key at ${url}, then call q402_doctor \u2014 it will walk the user through creating ~/.q402/mcp.env and pasting the key into the right slot.`;
|
|
777
850
|
}
|
|
778
851
|
var PAY_TOOL = {
|
|
779
852
|
name: "q402_pay",
|
|
@@ -960,7 +1033,7 @@ function describeSandboxReason2(resolvedKey, scope) {
|
|
|
960
1033
|
if (missing.length === 0) return "Sandbox mode active (no env state change needed).";
|
|
961
1034
|
const tier = scope === "trial" ? "Free Trial" : "Multichain";
|
|
962
1035
|
const url = scope === "trial" ? "https://q402.quackai.ai/event" : "https://q402.quackai.ai/payment";
|
|
963
|
-
return "Sandbox mode is active because the following env vars are missing or not yet set: " + missing.join(", ") + `. Get a live ${tier} key at ${url}.`;
|
|
1036
|
+
return "Sandbox mode is active because the following env vars are missing or not yet set: " + missing.join(", ") + `. Get a live ${tier} key at ${url}, then call q402_doctor \u2014 it will walk the user through creating ~/.q402/mcp.env and pasting the key into the right slot.`;
|
|
964
1037
|
}
|
|
965
1038
|
var BATCH_PAY_TOOL = {
|
|
966
1039
|
name: "q402_batch_pay",
|
|
@@ -1056,7 +1129,7 @@ async function runBalance() {
|
|
|
1056
1129
|
apiKeyMasked: null,
|
|
1057
1130
|
scopes: [],
|
|
1058
1131
|
dashboardUrl: "https://q402.quackai.ai/dashboard",
|
|
1059
|
-
setupHint: "
|
|
1132
|
+
setupHint: "No API key configured. Call q402_doctor for guided setup \u2014 it will offer to create ~/.q402/mcp.env with placeholders that the user can fill in. (Manual path: set Q402_TRIAL_API_KEY for BNB-only sponsored free trial (https://q402.quackai.ai/event) or Q402_MULTICHAIN_API_KEY for the paid 9-chain plan (https://q402.quackai.ai/payment).)"
|
|
1060
1133
|
};
|
|
1061
1134
|
}
|
|
1062
1135
|
const scopes = await Promise.all(
|
|
@@ -1453,9 +1526,311 @@ var CLEAR_DELEGATION_TOOL = {
|
|
|
1453
1526
|
}
|
|
1454
1527
|
};
|
|
1455
1528
|
|
|
1529
|
+
// src/tools/doctor.ts
|
|
1530
|
+
import { z as z8 } from "zod";
|
|
1531
|
+
import { Wallet as Wallet4 } from "ethers";
|
|
1532
|
+
var DoctorInputSchema = z8.object({});
|
|
1533
|
+
var ENV_FILE_TEMPLATE = `# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1534
|
+
# Q402 MCP \u2014 secrets
|
|
1535
|
+
# Read automatically by @quackai/q402-mcp on startup.
|
|
1536
|
+
# Edit this file in your editor. NEVER paste your private key into chat.
|
|
1537
|
+
# After editing, restart your MCP client (Codex / Claude / Cursor / Cline).
|
|
1538
|
+
# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1539
|
+
|
|
1540
|
+
# \u2500\u2500\u2500 API key \u2014 uncomment ONE (or both for auto-routing) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1541
|
+
# Free Trial: BNB Chain only, 2,000 sponsored TX
|
|
1542
|
+
# Get one at: https://q402.quackai.ai/event
|
|
1543
|
+
# Q402_TRIAL_API_KEY=q402_live_...
|
|
1544
|
+
|
|
1545
|
+
# Paid Multichain: all 9 chains, per-chain Gas Tank
|
|
1546
|
+
# Get one at: https://q402.quackai.ai/payment
|
|
1547
|
+
# Q402_MULTICHAIN_API_KEY=q402_live_...
|
|
1548
|
+
|
|
1549
|
+
# \u2500\u2500\u2500 Your wallet \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1550
|
+
# Hex EVM private key (0x + 64 hex chars). Signs payments LOCALLY on
|
|
1551
|
+
# your machine \u2014 never leaves your device, never sent to any server.
|
|
1552
|
+
# Q402_PRIVATE_KEY=0x...
|
|
1553
|
+
|
|
1554
|
+
# \u2500\u2500\u2500 Live mode flag \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1555
|
+
# Default 0 = sandbox (test responses, no funds move). Flip to 1 only
|
|
1556
|
+
# AFTER you've pasted real values into the lines above \u2014 otherwise the
|
|
1557
|
+
# server will refuse the placeholders.
|
|
1558
|
+
Q402_ENABLE_REAL_PAYMENTS=0
|
|
1559
|
+
|
|
1560
|
+
# \u2500\u2500\u2500 Q402 relay endpoint \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1561
|
+
# Default canonical Q402 deployment. Only change for self-hosted.
|
|
1562
|
+
Q402_RELAY_BASE_URL=https://q402.quackai.ai/api
|
|
1563
|
+
|
|
1564
|
+
# \u2500\u2500\u2500 Optional safety guards \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1565
|
+
# Max USD per single q402_pay call (default: 5)
|
|
1566
|
+
# Q402_MAX_AMOUNT_PER_CALL=5
|
|
1567
|
+
#
|
|
1568
|
+
# Comma-separated lowercase recipient allowlist (unset = any address OK)
|
|
1569
|
+
# Q402_ALLOWED_RECIPIENTS=0xabc...,0xdef...
|
|
1570
|
+
`;
|
|
1571
|
+
var SECURITY_NOTICE = "Q402 never asks you to paste your private key into chat. The MCP server signs payments LOCALLY on your machine \u2014 your key never leaves your device, never goes to a remote server. If a key was already pasted in chat by mistake, treat the wallet as exposed: move funds to a fresh wallet and use that new key in ~/.q402/mcp.env going forward.";
|
|
1572
|
+
function envSource(name) {
|
|
1573
|
+
if (process.env[name] !== void 0) return "process";
|
|
1574
|
+
if (Q402_ENV_FILE_KEYS.has(name)) return "file";
|
|
1575
|
+
if (ENV[name] !== void 0) return "file";
|
|
1576
|
+
return "unset";
|
|
1577
|
+
}
|
|
1578
|
+
function envSlot(name, purpose) {
|
|
1579
|
+
const source = envSource(name);
|
|
1580
|
+
return { set: source !== "unset", source, purpose };
|
|
1581
|
+
}
|
|
1582
|
+
function mask2(key) {
|
|
1583
|
+
if (!key || key.length < 12) return key ?? "";
|
|
1584
|
+
return `${key.slice(0, 12)}\u2026${key.slice(-4)}`;
|
|
1585
|
+
}
|
|
1586
|
+
function detectPhase() {
|
|
1587
|
+
const anyKey = !!(CONFIG.trialApiKey || CONFIG.multichainApiKey || CONFIG.legacyApiKey);
|
|
1588
|
+
const allEssentials = anyKey && !!CONFIG.privateKey && CONFIG.realPaymentsRequested && CONFIG.apiKeyKind === "live";
|
|
1589
|
+
if (allEssentials) return "live-check";
|
|
1590
|
+
if (anyKey || CONFIG.privateKey || CONFIG.realPaymentsRequested) return "needs-completion";
|
|
1591
|
+
return "first-install";
|
|
1592
|
+
}
|
|
1593
|
+
async function verifyOneKey(scope, envVar, apiKey) {
|
|
1594
|
+
const url = `${CONFIG.relayBaseUrl}/keys/verify`;
|
|
1595
|
+
try {
|
|
1596
|
+
const resp = await fetch(url, {
|
|
1597
|
+
method: "POST",
|
|
1598
|
+
headers: { "Content-Type": "application/json" },
|
|
1599
|
+
body: JSON.stringify({ apiKey }),
|
|
1600
|
+
signal: AbortSignal.timeout(1e4)
|
|
1601
|
+
});
|
|
1602
|
+
if (resp.status === 429) {
|
|
1603
|
+
return {
|
|
1604
|
+
scope,
|
|
1605
|
+
envVar,
|
|
1606
|
+
apiKeyMasked: mask2(apiKey),
|
|
1607
|
+
valid: false,
|
|
1608
|
+
error: "rate limited by relay \u2014 wait 60s and re-run q402_doctor"
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
if (!resp.ok) {
|
|
1612
|
+
return { scope, envVar, apiKeyMasked: mask2(apiKey), valid: false, error: `HTTP ${resp.status}` };
|
|
1613
|
+
}
|
|
1614
|
+
const body = await resp.json();
|
|
1615
|
+
const result = {
|
|
1616
|
+
scope,
|
|
1617
|
+
envVar,
|
|
1618
|
+
apiKeyMasked: mask2(apiKey),
|
|
1619
|
+
valid: body.valid ?? false,
|
|
1620
|
+
// Propagate the relay's specific reason ("API key has been rotated",
|
|
1621
|
+
// "Subscription expired", "Trial expired") so the user gets the
|
|
1622
|
+
// exact failure mode instead of a generic "verified as invalid".
|
|
1623
|
+
error: body.valid === false ? body.error : void 0,
|
|
1624
|
+
plan: body.plan,
|
|
1625
|
+
remainingCredits: body.remainingCredits,
|
|
1626
|
+
isTrial: body.isTrial,
|
|
1627
|
+
trialExpiresAt: body.trialExpiresAt,
|
|
1628
|
+
trialDaysLeft: body.trialDaysLeft
|
|
1629
|
+
};
|
|
1630
|
+
if (scope === "multichain" && body.isTrial) {
|
|
1631
|
+
result.slotWarning = "Trial-tier key found in Q402_MULTICHAIN_API_KEY slot. This works but you'll lose the auto-routing benefit (BNB free via Trial). Move the key to Q402_TRIAL_API_KEY in ~/.q402/mcp.env.";
|
|
1632
|
+
} else if (scope === "trial" && body.isTrial === false && body.plan && body.plan !== "trial") {
|
|
1633
|
+
result.slotWarning = "Paid Multichain-tier key found in Q402_TRIAL_API_KEY slot. BNB payments will burn your paid quota instead of using free Trial sponsorship. Move the key to Q402_MULTICHAIN_API_KEY in ~/.q402/mcp.env.";
|
|
1634
|
+
}
|
|
1635
|
+
return result;
|
|
1636
|
+
} catch (e) {
|
|
1637
|
+
return {
|
|
1638
|
+
scope,
|
|
1639
|
+
envVar,
|
|
1640
|
+
apiKeyMasked: mask2(apiKey),
|
|
1641
|
+
valid: false,
|
|
1642
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
async function pingRelay() {
|
|
1647
|
+
const url = `${CONFIG.relayBaseUrl}/keys/verify`;
|
|
1648
|
+
const t0 = Date.now();
|
|
1649
|
+
try {
|
|
1650
|
+
const resp = await fetch(url, {
|
|
1651
|
+
method: "POST",
|
|
1652
|
+
headers: { "Content-Type": "application/json" },
|
|
1653
|
+
body: "{}",
|
|
1654
|
+
signal: AbortSignal.timeout(1e4)
|
|
1655
|
+
});
|
|
1656
|
+
const reachable = resp.status >= 200 && resp.status < 500;
|
|
1657
|
+
return { url, reachable, latencyMs: Date.now() - t0 };
|
|
1658
|
+
} catch (e) {
|
|
1659
|
+
return {
|
|
1660
|
+
url,
|
|
1661
|
+
reachable: false,
|
|
1662
|
+
latencyMs: Date.now() - t0,
|
|
1663
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
async function fetchDelegation(address) {
|
|
1668
|
+
const url = `${CONFIG.relayBaseUrl}/wallet/delegation-status?address=${address}`;
|
|
1669
|
+
try {
|
|
1670
|
+
const resp = await fetch(url, { signal: AbortSignal.timeout(1e4) });
|
|
1671
|
+
if (!resp.ok) {
|
|
1672
|
+
return CHAIN_KEYS.map((chain) => ({ chain, delegated: false, error: `HTTP ${resp.status}` }));
|
|
1673
|
+
}
|
|
1674
|
+
const body = await resp.json();
|
|
1675
|
+
return CHAIN_KEYS.map((chain) => {
|
|
1676
|
+
const s = body.chains?.[chain];
|
|
1677
|
+
if (!s) return { chain, delegated: false, error: "missing from response" };
|
|
1678
|
+
return { chain, delegated: s.delegated, impl: s.impl, error: s.error };
|
|
1679
|
+
});
|
|
1680
|
+
} catch (e) {
|
|
1681
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
1682
|
+
return CHAIN_KEYS.map((chain) => ({ chain, delegated: false, error }));
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
async function runDoctor() {
|
|
1686
|
+
const phase = detectPhase();
|
|
1687
|
+
const envFile = {
|
|
1688
|
+
path: Q402_ENV_FILE_PATH,
|
|
1689
|
+
exists: Q402_ENV_FILE_PRESENT
|
|
1690
|
+
};
|
|
1691
|
+
const envState = {
|
|
1692
|
+
Q402_TRIAL_API_KEY: envSlot(
|
|
1693
|
+
"Q402_TRIAL_API_KEY",
|
|
1694
|
+
"Free Trial \u2014 BNB only, 2,000 sponsored TX. Get at https://q402.quackai.ai/event"
|
|
1695
|
+
),
|
|
1696
|
+
Q402_MULTICHAIN_API_KEY: envSlot(
|
|
1697
|
+
"Q402_MULTICHAIN_API_KEY",
|
|
1698
|
+
"Paid Multichain \u2014 all 9 chains, per-chain Gas Tank. Get at https://q402.quackai.ai/payment"
|
|
1699
|
+
),
|
|
1700
|
+
Q402_PRIVATE_KEY: envSlot(
|
|
1701
|
+
"Q402_PRIVATE_KEY",
|
|
1702
|
+
"Hex EVM private key. Signs LOCALLY on your machine \u2014 never leaves your device."
|
|
1703
|
+
),
|
|
1704
|
+
Q402_ENABLE_REAL_PAYMENTS: envSlot(
|
|
1705
|
+
"Q402_ENABLE_REAL_PAYMENTS",
|
|
1706
|
+
"Must be '1' to allow real TX. Anything else = test response (fake hash)."
|
|
1707
|
+
)
|
|
1708
|
+
};
|
|
1709
|
+
const missing = [];
|
|
1710
|
+
if (!CONFIG.trialApiKey && !CONFIG.multichainApiKey && !CONFIG.legacyApiKey) {
|
|
1711
|
+
missing.push(
|
|
1712
|
+
"An API key (Q402_TRIAL_API_KEY for free BNB OR Q402_MULTICHAIN_API_KEY for paid 9-chain)"
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
if (!CONFIG.privateKey) {
|
|
1716
|
+
missing.push("Q402_PRIVATE_KEY");
|
|
1717
|
+
} else if (!isValidPrivateKey(CONFIG.privateKey)) {
|
|
1718
|
+
missing.push(
|
|
1719
|
+
"Q402_PRIVATE_KEY is set but malformed (expected 0x + 64 hex chars). Looks like the placeholder '0x...' is still in ~/.q402/mcp.env \u2014 paste a real key in your editor."
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1722
|
+
if (!CONFIG.realPaymentsRequested) missing.push("Q402_ENABLE_REAL_PAYMENTS=1");
|
|
1723
|
+
const recommendedActions = [];
|
|
1724
|
+
if (!envFile.exists) {
|
|
1725
|
+
recommendedActions.push({
|
|
1726
|
+
id: "create-env-file",
|
|
1727
|
+
type: "write_file",
|
|
1728
|
+
path: Q402_ENV_FILE_PATH,
|
|
1729
|
+
createParentDirs: true,
|
|
1730
|
+
content: ENV_FILE_TEMPLATE,
|
|
1731
|
+
requiresUserConfirm: true,
|
|
1732
|
+
description: "Create ~/.q402/mcp.env with placeholder values, then open it in the user's editor.",
|
|
1733
|
+
ifExists: "skip"
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
const warnings = [];
|
|
1737
|
+
for (const name of Q402_ENV_FILE_KEYS_ALL) {
|
|
1738
|
+
if (process.env[name] !== void 0 && Q402_ENV_FILE_KEYS_ALL.has(name) && !Q402_ENV_FILE_KEYS.has(name)) {
|
|
1739
|
+
warnings.push(
|
|
1740
|
+
`${name} is set in both your shell (process.env) AND ~/.q402/mcp.env \u2014 the shell value wins. Editing the file will have NO effect until you \`unset ${name}\` in your shell (or update the shell value to match).`
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
if (phase !== "live-check") {
|
|
1745
|
+
return {
|
|
1746
|
+
package: PACKAGE_NAME,
|
|
1747
|
+
version: PACKAGE_VERSION,
|
|
1748
|
+
phase,
|
|
1749
|
+
ready: false,
|
|
1750
|
+
envFile,
|
|
1751
|
+
envState,
|
|
1752
|
+
missing,
|
|
1753
|
+
warnings,
|
|
1754
|
+
recommendedActions,
|
|
1755
|
+
greeting: phase === "first-install" ? `Q402 MCP is installed (v${PACKAGE_VERSION}).` : `Q402 MCP is installed (v${PACKAGE_VERSION}) \u2014 partially configured.`,
|
|
1756
|
+
nextStep: phase === "first-install" ? "Offer to create ~/.q402/mcp.env. After yes, run the recommendedActions[].write_file action, then open the file in the user's editor (e.g. via `code` / `open` / `start` / `xdg-open`). Then walk through filling in the API key and private key, one at a time. Do NOT accept key values via chat \u2014 direct the user to edit the file in their editor." : `Tell the user which env vars are still missing (from the 'missing' list) and how to add them to ~/.q402/mcp.env. Restart needed after editing.`,
|
|
1757
|
+
securityNotice: SECURITY_NOTICE
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
let walletAddress;
|
|
1761
|
+
let walletError;
|
|
1762
|
+
try {
|
|
1763
|
+
walletAddress = new Wallet4(CONFIG.privateKey).address;
|
|
1764
|
+
} catch (e) {
|
|
1765
|
+
walletError = e instanceof Error ? e.message : String(e);
|
|
1766
|
+
warnings.push(
|
|
1767
|
+
`Q402_PRIVATE_KEY is set but does not parse as a 32-byte hex key: ${walletError}. Open ~/.q402/mcp.env in your editor and paste a real key (0x + 64 hex chars). Live calls will fail until this is fixed.`
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1770
|
+
const verifyTargets = [];
|
|
1771
|
+
if (CONFIG.trialApiKey) verifyTargets.push({ scope: "trial", envVar: "Q402_TRIAL_API_KEY", key: CONFIG.trialApiKey });
|
|
1772
|
+
if (CONFIG.multichainApiKey) verifyTargets.push({ scope: "multichain", envVar: "Q402_MULTICHAIN_API_KEY", key: CONFIG.multichainApiKey });
|
|
1773
|
+
if (verifyTargets.length === 0 && CONFIG.legacyApiKey) {
|
|
1774
|
+
verifyTargets.push({ scope: "legacy", envVar: "Q402_API_KEY", key: CONFIG.legacyApiKey });
|
|
1775
|
+
}
|
|
1776
|
+
const [keys, delegation, relay] = await Promise.all([
|
|
1777
|
+
Promise.all(verifyTargets.map((t) => verifyOneKey(t.scope, t.envVar, t.key))),
|
|
1778
|
+
walletAddress ? fetchDelegation(walletAddress) : Promise.resolve(void 0),
|
|
1779
|
+
pingRelay()
|
|
1780
|
+
]);
|
|
1781
|
+
for (const k of keys) if (k.slotWarning) warnings.push(k.slotWarning);
|
|
1782
|
+
for (const k of keys) {
|
|
1783
|
+
if (typeof k.remainingCredits === "number" && k.remainingCredits === 0) {
|
|
1784
|
+
warnings.push(
|
|
1785
|
+
`${k.envVar} has 0 credits remaining. ` + (k.isTrial ? "Trial allotment exhausted \u2014 upgrade to a Multichain plan at https://q402.quackai.ai/payment." : "Paid plan quota exhausted \u2014 top up at https://q402.quackai.ai/dashboard?tab=billing.")
|
|
1786
|
+
);
|
|
1787
|
+
} else if (typeof k.remainingCredits === "number" && k.remainingCredits > 0 && k.remainingCredits < 50) {
|
|
1788
|
+
warnings.push(
|
|
1789
|
+
`${k.envVar} has only ${k.remainingCredits} credits left \u2014 top up before you run out.`
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
if (!k.valid) {
|
|
1793
|
+
warnings.push(
|
|
1794
|
+
k.error ? `${k.envVar}: ${k.error}.` : `${k.envVar} verified as invalid by the relay \u2014 check the key value in ~/.q402/mcp.env.`
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
if (relay && !relay.reachable) {
|
|
1799
|
+
warnings.push(
|
|
1800
|
+
`Q402 relay at ${relay.url} is unreachable. Check your network or override with Q402_RELAY_BASE_URL if you self-host.`
|
|
1801
|
+
);
|
|
1802
|
+
}
|
|
1803
|
+
const ready = warnings.length === 0 && keys.some((k) => k.valid);
|
|
1804
|
+
return {
|
|
1805
|
+
package: PACKAGE_NAME,
|
|
1806
|
+
version: PACKAGE_VERSION,
|
|
1807
|
+
phase,
|
|
1808
|
+
ready,
|
|
1809
|
+
envFile,
|
|
1810
|
+
envState,
|
|
1811
|
+
missing,
|
|
1812
|
+
wallet: walletAddress ? { address: walletAddress } : void 0,
|
|
1813
|
+
keys,
|
|
1814
|
+
delegation,
|
|
1815
|
+
relay,
|
|
1816
|
+
warnings,
|
|
1817
|
+
recommendedActions,
|
|
1818
|
+
greeting: ready ? `Q402 MCP is ready (v${PACKAGE_VERSION}).` : `Q402 MCP is installed but has ${warnings.length} issue${warnings.length === 1 ? "" : "s"} to address.`,
|
|
1819
|
+
nextStep: ready ? "Summarize the wallet address, plan tier(s), remaining quota, and any non-zero delegation counts to the user as a checklist. Then offer to make a test payment via q402_quote." : "Walk the user through each warning in order. For slot-mismatch warnings, the fix is editing ~/.q402/mcp.env and restarting the client.",
|
|
1820
|
+
securityNotice: SECURITY_NOTICE
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
var DOCTOR_TOOL = {
|
|
1824
|
+
name: "q402_doctor",
|
|
1825
|
+
description: 'Run a Q402 health check \u2014 covers first-install onboarding AND ongoing diagnostics in one tool. Read-only, no API key required. Detects the current phase (first-install / needs-completion / live-check) and tailors output to it. \n\nUse when the user says any of: "set up Q402", "verify Q402", "why isn\'t Q402 working", "Q402 status", "check Q402". This is the FIRST tool to call after install, BEFORE q402_pay or q402_balance \u2014 it tells the agent what state the user is in. \n\nMulti-turn pattern the AI should follow when phase = first-install: (1) Tell user MCP is installed. (2) Ask one yes/no question: \'Want me to create your secrets file at ~/.q402/mcp.env?\' (3) On yes, execute the recommendedActions[].write_file action using the client\'s own filesystem tool, then open the file in the user\'s editor (e.g. `code ~/.q402/mcp.env`, `open` on macOS, `start` on Windows, `xdg-open` on Linux). (4) Guide the user through getting an API key (free Trial at https://q402.quackai.ai/event OR paid Multichain at /payment) and pasting it into the file (in their editor \u2014 NEVER in chat). (5) Same for the private key. (6) Tell them to save + restart the MCP client. (7) Call q402_doctor again to verify. \n\nSecurity policy carried in the response: AI MUST surface the securityNotice when first walking through setup. If the user pastes a private key directly in chat, DO NOT refuse \u2014 the exposure already happened. Help them by directing them to put it in the file themselves (via their editor), and inform them the chat history now contains the key (most clients store this locally, some sync to cloud) so they should treat the wallet as exposed if it holds valuables. \n\nLive-check phase additionally returns per-scope quota, EIP-7702 delegation state per chain, relay reachability, and slot-mismatch warnings (e.g. Trial key in Multichain slot silently burns paid quota \u2014 surface this to the user).',
|
|
1826
|
+
inputSchema: {
|
|
1827
|
+
type: "object",
|
|
1828
|
+
properties: {},
|
|
1829
|
+
additionalProperties: false
|
|
1830
|
+
}
|
|
1831
|
+
};
|
|
1832
|
+
|
|
1456
1833
|
// src/index.ts
|
|
1457
|
-
var PACKAGE_NAME = "@quackai/q402-mcp";
|
|
1458
|
-
var PACKAGE_VERSION = "0.5.5";
|
|
1459
1834
|
function jsonText(value) {
|
|
1460
1835
|
return { type: "text", text: JSON.stringify(value, null, 2) };
|
|
1461
1836
|
}
|
|
@@ -1466,6 +1841,9 @@ async function main() {
|
|
|
1466
1841
|
);
|
|
1467
1842
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1468
1843
|
tools: [
|
|
1844
|
+
// doctor first — it's the bootstrap tool: any "set up Q402" / "is Q402
|
|
1845
|
+
// working" prompt should land here before quote/balance/pay are tried.
|
|
1846
|
+
DOCTOR_TOOL,
|
|
1469
1847
|
QUOTE_TOOL,
|
|
1470
1848
|
BALANCE_TOOL,
|
|
1471
1849
|
PAY_TOOL,
|
|
@@ -1479,6 +1857,10 @@ async function main() {
|
|
|
1479
1857
|
const { name, arguments: args } = req.params;
|
|
1480
1858
|
try {
|
|
1481
1859
|
switch (name) {
|
|
1860
|
+
case "q402_doctor": {
|
|
1861
|
+
DoctorInputSchema.parse(args ?? {});
|
|
1862
|
+
return { content: [jsonText(await runDoctor())] };
|
|
1863
|
+
}
|
|
1482
1864
|
case "q402_quote": {
|
|
1483
1865
|
const parsed = QuoteInputSchema.parse(args ?? {});
|
|
1484
1866
|
return { content: [jsonText(runQuote(parsed))] };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quackai/q402-mcp",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"description": "MCP server for Q402 — gasless USDC, USDT, and RLUSD payments across 9 EVM chains, 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": [
|