@yugabytedb/meko-mcp 1.0.5 → 1.0.8
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 +8 -4
- package/assets/hooks-handlers/lib/capture.js +1 -1
- package/bin/create-meko-setup.mjs +11 -9
- package/lib/clients/aliases.mjs +30 -0
- package/lib/clients/claude-code.mjs +8 -3
- package/lib/clients/claude-desktop.mjs +8 -7
- package/lib/clients/cursor.mjs +2 -2
- package/lib/mcp-transport.mjs +2 -2
- package/lib/migrate/mcp-client.mjs +1 -1
- package/lib/migrate/orchestrator.mjs +45 -3
- package/lib/prompt.mjs +36 -76
- package/lib/validate.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,6 +4,10 @@ A zero-dependency Node.js CLI that configures [Meko MCP Server](https://mekodata
|
|
|
4
4
|
|
|
5
5
|
Reduces manual setup to a single command.
|
|
6
6
|
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- **Node.js 18 or newer** on your PATH. Get it from [nodejs.org](https://nodejs.org/) or via a version manager (`nvm`, `fnm`, `asdf`). `npx`, which comes bundled with Node, is how you invoke the installer.
|
|
10
|
+
|
|
7
11
|
## Quick Start
|
|
8
12
|
|
|
9
13
|
The installer is path-agnostic — the three forms below are equivalent. Pick the one that matches how you obtained the code:
|
|
@@ -77,9 +81,9 @@ The installer performs up to 5 steps (some are skippable via flags):
|
|
|
77
81
|
|
|
78
82
|
| Step | Action | Command / File Modified |
|
|
79
83
|
|------|--------|------------------------|
|
|
80
|
-
| 1 | Register MCP server | `claude mcp add meko <url> --transport http --scope user` (with `-H "
|
|
84
|
+
| 1 | Register MCP server | `claude mcp add meko <url> --transport http --scope user` (with `-H "Authorization: Bearer <key>"` for cloud) |
|
|
81
85
|
| 2 | Set hook environment vars | Writes `MEKO_MCP_URL` and `MEKO_API_KEY` into `~/.claude/settings.json` under the `env` key |
|
|
82
|
-
| 3 | Register plugin marketplace | `claude plugin marketplace add yugabyte/meko-mcp-server --
|
|
86
|
+
| 3 | Register plugin marketplace | `claude plugin marketplace add yugabyte/meko-mcp-server --scope user` |
|
|
83
87
|
| 4 | Install skills plugin | `claude plugin install meko-agent-skills --scope user` |
|
|
84
88
|
| 5 | Validate setup | Config check via `claude mcp list`, MCP `initialize` handshake, `tools/list` verification |
|
|
85
89
|
|
|
@@ -288,7 +292,7 @@ memory errs: 0 (failed memory_add calls; re-run with --force to
|
|
|
288
292
|
|---|-----------|-----------|
|
|
289
293
|
| **URL** | Provided by app.mekodev.com > Datapacks > Connect | `http://localhost:8000/mcp` |
|
|
290
294
|
| **Authentication** | Requires `--api-key` or `$MEKO_API_KEY` env var | None |
|
|
291
|
-
| **MCP registration** | Includes `-H "
|
|
295
|
+
| **MCP registration** | Includes `-H "Authorization: Bearer <key>"` header | No auth header |
|
|
292
296
|
| **Hook env vars** | Both `MEKO_MCP_URL` and `MEKO_API_KEY` written to settings | Only `MEKO_MCP_URL` written |
|
|
293
297
|
| **Infrastructure** | Hosted, no local setup needed | Requires `docker compose up -d` running the Meko stack |
|
|
294
298
|
| **Use case** | Production, team collaboration | Development, testing |
|
|
@@ -457,7 +461,7 @@ node installer/bin/create-meko-setup.mjs --uninstall --verbose
|
|
|
457
461
|
| # | Scenario | Steps | Expected Result |
|
|
458
462
|
|---|----------|-------|-----------------|
|
|
459
463
|
| M1 | Fresh local install | `node installer/bin/create-meko-setup.mjs --local --yes` with docker stack running | All 5 steps pass, validation shows 3 green checks |
|
|
460
|
-
| M2 | Fresh cloud install | Run interactively, provide Cloud Meko URL + API key | MCP registered with
|
|
464
|
+
| M2 | Fresh cloud install | Run interactively, provide Cloud Meko URL + API key | MCP registered with `Authorization: Bearer` header, env vars set, validation passes |
|
|
461
465
|
| M3 | Existing config | Run installer when `meko` MCP is already registered | Prompts to overwrite; answering No keeps existing config |
|
|
462
466
|
| M4 | No claude CLI | Run on a machine without `claude` in PATH | Exits with error message and install link |
|
|
463
467
|
| M5 | Server unreachable | Install with `--skip-validation`, then re-run without it | Install succeeds; validation warns about connectivity |
|
|
@@ -274,7 +274,7 @@ function mcpPost(jsonBody) {
|
|
|
274
274
|
"Content-Length": Buffer.byteLength(body),
|
|
275
275
|
};
|
|
276
276
|
if (MEKO_API_KEY) {
|
|
277
|
-
headers["
|
|
277
|
+
headers["Authorization"] = `Bearer ${MEKO_API_KEY}`;
|
|
278
278
|
}
|
|
279
279
|
if (mcpSessionId) {
|
|
280
280
|
headers["Mcp-Session-Id"] = mcpSessionId;
|
|
@@ -10,6 +10,7 @@ import { askChoice, askMultiChoice, askText, askSecret, askYesNo, close as close
|
|
|
10
10
|
import { banner, error, dim } from '../lib/logger.mjs';
|
|
11
11
|
import { isPackagedInstall, selfInvocation } from '../lib/clients/common.mjs';
|
|
12
12
|
import { getClientAdapters } from '../lib/clients/index.mjs';
|
|
13
|
+
import { parseClientList } from '../lib/clients/aliases.mjs';
|
|
13
14
|
import { startMigration, runChildMigration, readStatus, stopMigration } from '../lib/migrate.mjs';
|
|
14
15
|
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
@@ -90,6 +91,7 @@ Examples:
|
|
|
90
91
|
|
|
91
92
|
Options:
|
|
92
93
|
--client <list> Target clients: one or more of claude-code, claude-desktop, cursor.
|
|
94
|
+
"claude" is accepted as an alias for claude-code.
|
|
93
95
|
Comma-separated, or "all" to target every supported client (default: claude-code)
|
|
94
96
|
--url <url> MCP server URL (from app.mekodev.com > Datapacks > Connect)
|
|
95
97
|
--api-key <key> API key for Cloud Meko authentication
|
|
@@ -131,17 +133,12 @@ function isCloudUrl(url) {
|
|
|
131
133
|
/**
|
|
132
134
|
* Parse --client into a deduplicated list of client IDs.
|
|
133
135
|
* Accepts a single value, a comma-separated list, or the keyword "all".
|
|
136
|
+
* Aliases (see CLIENT_ALIASES in ../lib/clients.mjs) are resolved before the
|
|
137
|
+
* validity check so the cloud UI snippet (`--client claude`) works verbatim.
|
|
134
138
|
* Returns { clients, invalid } where invalid lists any unknown tokens.
|
|
135
139
|
*/
|
|
136
140
|
function parseClients(raw) {
|
|
137
|
-
|
|
138
|
-
if (!trimmed) return { clients: ['claude-code'], invalid: [] };
|
|
139
|
-
if (trimmed.toLowerCase() === 'all') return { clients: [...ALL_CLIENTS], invalid: [] };
|
|
140
|
-
|
|
141
|
-
const tokens = trimmed.split(',').map((t) => t.trim()).filter(Boolean);
|
|
142
|
-
const invalid = tokens.filter((t) => !ALL_CLIENTS.includes(t));
|
|
143
|
-
const clients = [...new Set(tokens.filter((t) => ALL_CLIENTS.includes(t)))];
|
|
144
|
-
return { clients, invalid };
|
|
141
|
+
return parseClientList(raw, ALL_CLIENTS);
|
|
145
142
|
}
|
|
146
143
|
|
|
147
144
|
function isValidScope(scope) {
|
|
@@ -374,6 +371,8 @@ async function main() {
|
|
|
374
371
|
// Step 1 — connection type. Local Meko only makes sense for users who
|
|
375
372
|
// have the server repo cloned locally (docker-compose stack on :8000);
|
|
376
373
|
// npm/npx users have no way to run it, so hide the option for them.
|
|
374
|
+
// When only Cloud is available, skip the prompt entirely — asking to
|
|
375
|
+
// pick between one option is pointless UX.
|
|
377
376
|
const connectionOptions = [
|
|
378
377
|
{ label: 'Cloud Meko', description: 'Hosted at mekodev.com (requires URL + API key)' },
|
|
379
378
|
];
|
|
@@ -383,7 +382,10 @@ async function main() {
|
|
|
383
382
|
description: 'Local stack at localhost:8000',
|
|
384
383
|
});
|
|
385
384
|
}
|
|
386
|
-
const connectionType =
|
|
385
|
+
const connectionType =
|
|
386
|
+
connectionOptions.length === 1
|
|
387
|
+
? connectionOptions[0].label
|
|
388
|
+
: await askChoice('Connection type:', connectionOptions);
|
|
387
389
|
|
|
388
390
|
if (connectionType === 'Cloud Meko') {
|
|
389
391
|
// Step 2 — MCP URL
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Tokens the cloud UI or older docs pass on `--client` that should map to a
|
|
2
|
+
// canonical adapter ID. Kept in a tiny, side-effect-free module so tests can
|
|
3
|
+
// import it without booting the bin entry point.
|
|
4
|
+
export const CLIENT_ALIASES = Object.freeze({
|
|
5
|
+
claude: "claude-code",
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse a `--client` argument into a deduplicated list of client IDs.
|
|
10
|
+
*
|
|
11
|
+
* @param {string|null|undefined} raw - Raw CLI argument value.
|
|
12
|
+
* @param {string[]} allClients - Canonical IDs accepted by the installer.
|
|
13
|
+
* @returns {{clients: string[], invalid: string[]}}
|
|
14
|
+
*/
|
|
15
|
+
export function parseClientList(raw, allClients) {
|
|
16
|
+
const trimmed = (raw ?? "").trim();
|
|
17
|
+
if (!trimmed) return { clients: ["claude-code"], invalid: [] };
|
|
18
|
+
if (trimmed.toLowerCase() === "all") {
|
|
19
|
+
return { clients: [...allClients], invalid: [] };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const tokens = trimmed
|
|
23
|
+
.split(",")
|
|
24
|
+
.map((t) => t.trim())
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
.map((t) => CLIENT_ALIASES[t] ?? t);
|
|
27
|
+
const invalid = tokens.filter((t) => !allClients.includes(t));
|
|
28
|
+
const clients = [...new Set(tokens.filter((t) => allClients.includes(t)))];
|
|
29
|
+
return { clients, invalid };
|
|
30
|
+
}
|
|
@@ -89,14 +89,14 @@ export function createClaudeCodeAdapter(options = {}) {
|
|
|
89
89
|
buildRegisterCommand({ name, url, scope, apiKey }) {
|
|
90
90
|
let cmd = `claude mcp add ${name} ${url} --transport http --scope ${scope}`;
|
|
91
91
|
if (apiKey) {
|
|
92
|
-
cmd += ` -H "
|
|
92
|
+
cmd += ` -H "Authorization: Bearer ${apiKey}"`;
|
|
93
93
|
}
|
|
94
94
|
return cmd;
|
|
95
95
|
},
|
|
96
96
|
buildRegisterDisplayCommand({ name, url, scope, apiKey }) {
|
|
97
97
|
let displayCmd = `claude mcp add ${name} ${url} --transport http --scope ${scope}`;
|
|
98
98
|
if (apiKey) {
|
|
99
|
-
displayCmd += ` -H "
|
|
99
|
+
displayCmd += ` -H "Authorization: Bearer ${redactKey(apiKey)}"`;
|
|
100
100
|
}
|
|
101
101
|
return displayCmd;
|
|
102
102
|
},
|
|
@@ -145,8 +145,13 @@ export function createClaudeCodeAdapter(options = {}) {
|
|
|
145
145
|
}
|
|
146
146
|
},
|
|
147
147
|
installSkills({ scope, dryRun, verbose, log }) {
|
|
148
|
+
// NB: we deliberately do NOT pass --sparse. Claude CLI's sparse
|
|
149
|
+
// path filter silently produces an empty clone for our layout,
|
|
150
|
+
// leaving the marketplace.json missing. Full clone is small
|
|
151
|
+
// (~few MB, mostly markdown) and is the only way that reliably
|
|
152
|
+
// succeeds today.
|
|
148
153
|
const marketplaceCmd =
|
|
149
|
-
`claude plugin marketplace add yugabyte/meko-mcp-server --
|
|
154
|
+
`claude plugin marketplace add yugabyte/meko-mcp-server --scope ${scope}`;
|
|
150
155
|
const pluginCmd = `claude plugin install meko-agent-skills --scope ${scope}`;
|
|
151
156
|
|
|
152
157
|
if (dryRun) {
|
|
@@ -24,13 +24,13 @@ import { getRepoRoot, run } from "./common.mjs";
|
|
|
24
24
|
* "meko": {
|
|
25
25
|
* "command": "npx",
|
|
26
26
|
* "args": ["-y", "mcp-remote", "<url>", "--transport", "http-only",
|
|
27
|
-
* "--header", "
|
|
27
|
+
* "--header", "Authorization:Bearer ${MEKO_API_KEY}"],
|
|
28
28
|
* "env": { "MEKO_API_KEY": "<key>" }
|
|
29
29
|
* }
|
|
30
30
|
*
|
|
31
31
|
* The header uses the `${ENV_VAR}` interpolation form (documented by
|
|
32
|
-
* mcp-remote) with no space around the colon
|
|
33
|
-
* space-containing `--header` args.
|
|
32
|
+
* mcp-remote) with no space around the colon after the header name —
|
|
33
|
+
* some clients mis-parse space-containing `--header` args.
|
|
34
34
|
*
|
|
35
35
|
* installSkills() runs skills/scripts/package-desktop-skill.sh to build the
|
|
36
36
|
* .skill bundle and prints import instructions. Desktop has no programmatic
|
|
@@ -228,10 +228,11 @@ export function buildMcpRemoteEntry({ url, apiKey }) {
|
|
|
228
228
|
const args = ["-y", "mcp-remote", url, "--transport", "http-only"];
|
|
229
229
|
const entry = { command: "npx", args };
|
|
230
230
|
if (apiKey) {
|
|
231
|
-
// `
|
|
232
|
-
// docs call out a spacing bug in some
|
|
233
|
-
//
|
|
234
|
-
|
|
231
|
+
// `Authorization:Bearer ${MEKO_API_KEY}` with no space around the colon
|
|
232
|
+
// after the header name — mcp-remote docs call out a spacing bug in some
|
|
233
|
+
// clients' arg parsing. The space between `Bearer` and the token value
|
|
234
|
+
// is part of the header value and is required by RFC 6750.
|
|
235
|
+
args.push("--header", "Authorization:Bearer ${MEKO_API_KEY}");
|
|
235
236
|
entry.env = { MEKO_API_KEY: apiKey };
|
|
236
237
|
}
|
|
237
238
|
return entry;
|
package/lib/clients/cursor.mjs
CHANGED
|
@@ -81,7 +81,7 @@ export function createCursorAdapter(customPaths = {}) {
|
|
|
81
81
|
log.dim(` Would update ${mcpPath}:`);
|
|
82
82
|
log.dim(` mcpServers.${name}.url = ${url}`);
|
|
83
83
|
if (apiKey) {
|
|
84
|
-
log.dim(" mcpServers.<name>.headers.
|
|
84
|
+
log.dim(" mcpServers.<name>.headers.Authorization = <set>");
|
|
85
85
|
}
|
|
86
86
|
return;
|
|
87
87
|
}
|
|
@@ -92,7 +92,7 @@ export function createCursorAdapter(customPaths = {}) {
|
|
|
92
92
|
}
|
|
93
93
|
const server = { url };
|
|
94
94
|
if (apiKey) {
|
|
95
|
-
server.headers = {
|
|
95
|
+
server.headers = { Authorization: `Bearer ${apiKey}` };
|
|
96
96
|
}
|
|
97
97
|
cfg.mcpServers[name] = server;
|
|
98
98
|
writeJsonFile(mcpPath, cfg);
|
package/lib/mcp-transport.mjs
CHANGED
|
@@ -17,7 +17,7 @@ export const DEFAULT_REQUEST_TIMEOUT_MS = 10_000;
|
|
|
17
17
|
*
|
|
18
18
|
* @param {string} url - Target URL.
|
|
19
19
|
* @param {object} payload - JSON-RPC body.
|
|
20
|
-
* @param {string|null} apiKey - Optional API key, sent as `
|
|
20
|
+
* @param {string|null} apiKey - Optional API key, sent as `Authorization: Bearer <key>`.
|
|
21
21
|
* @param {string|null} sessionId - Optional `Mcp-Session-Id` to include.
|
|
22
22
|
* @param {object} [opts]
|
|
23
23
|
* @param {number} [opts.timeoutMs] - Override per-request timeout.
|
|
@@ -34,7 +34,7 @@ export function jsonRpcRequest(url, payload, apiKey, sessionId, opts = {}) {
|
|
|
34
34
|
Accept: "application/json",
|
|
35
35
|
};
|
|
36
36
|
if (apiKey) {
|
|
37
|
-
headers["
|
|
37
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
38
38
|
}
|
|
39
39
|
if (sessionId) {
|
|
40
40
|
headers["Mcp-Session-Id"] = sessionId;
|
|
@@ -140,7 +140,7 @@ export class McpClient {
|
|
|
140
140
|
|
|
141
141
|
if (res.statusCode === 401 || res.statusCode === 403) {
|
|
142
142
|
const e = new Error(
|
|
143
|
-
`Auth rejected by server (HTTP ${res.statusCode}). Check
|
|
143
|
+
`Auth rejected by server (HTTP ${res.statusCode}). Check Authorization header / API key.`,
|
|
144
144
|
);
|
|
145
145
|
e.statusCode = res.statusCode;
|
|
146
146
|
throw e;
|
|
@@ -283,11 +283,17 @@ export async function runMigration(opts) {
|
|
|
283
283
|
const [, memRes] = await Promise.all([addMsgPromise, memPromise]);
|
|
284
284
|
messagesSent++;
|
|
285
285
|
if (!opts.noExtract) {
|
|
286
|
-
if (memRes && memRes.ok) {
|
|
286
|
+
if (memRes && memRes.ok && !isErrorText(memRes.text)) {
|
|
287
287
|
memoryPairsSent++;
|
|
288
288
|
prog.counts.memoryPairsSent++;
|
|
289
289
|
} else if (memRes) {
|
|
290
290
|
prog.counts.memoryErrors++;
|
|
291
|
+
log.warn("memory_add_failed", {
|
|
292
|
+
agentId: ref.agentId,
|
|
293
|
+
sessionUuid: ref.sessionUuid,
|
|
294
|
+
userUuid: ex.user_uuid,
|
|
295
|
+
detail: errorDetail(memRes),
|
|
296
|
+
});
|
|
291
297
|
}
|
|
292
298
|
}
|
|
293
299
|
lastUuid = ex.user_uuid;
|
|
@@ -361,7 +367,7 @@ export async function runMigration(opts) {
|
|
|
361
367
|
metadata: JSON.stringify(item.metadata),
|
|
362
368
|
}, { fireAndForget: true });
|
|
363
369
|
|
|
364
|
-
if (res.ok) {
|
|
370
|
+
if (res.ok && !isErrorText(res.text)) {
|
|
365
371
|
state.markMemory(st, item.agentId, item.hash, {
|
|
366
372
|
memoryId: extractId(res.text) || "",
|
|
367
373
|
name: item.name,
|
|
@@ -370,7 +376,12 @@ export async function runMigration(opts) {
|
|
|
370
376
|
prog.counts.memoriesFromMD++;
|
|
371
377
|
} else {
|
|
372
378
|
prog.counts.memoryErrors++;
|
|
373
|
-
log.warn("memory_md_failed", {
|
|
379
|
+
log.warn("memory_md_failed", {
|
|
380
|
+
agentId: item.agentId,
|
|
381
|
+
name: item.name,
|
|
382
|
+
error: res.error?.message,
|
|
383
|
+
detail: errorDetail(res),
|
|
384
|
+
});
|
|
374
385
|
}
|
|
375
386
|
if (++opsSinceFlush >= FLUSH_EVERY) flushState();
|
|
376
387
|
writeProgress();
|
|
@@ -441,3 +452,34 @@ function extractId(text) {
|
|
|
441
452
|
return null;
|
|
442
453
|
}
|
|
443
454
|
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* The MCP transport returns `{ok:true}` whenever the HTTP call itself
|
|
458
|
+
* succeeded, even if the tool's body encoded a failure. memory_add in
|
|
459
|
+
* older server builds returned plain "Error adding memory: ..." strings
|
|
460
|
+
* on exception; newer builds return JSON `{"error":"memory_add_failed",...}`.
|
|
461
|
+
* Either way, the text content is the authoritative success signal.
|
|
462
|
+
*/
|
|
463
|
+
function isErrorText(text) {
|
|
464
|
+
if (!text) return false;
|
|
465
|
+
const trimmed = text.trim();
|
|
466
|
+
if (trimmed.startsWith("Error adding memory")) return true;
|
|
467
|
+
if (trimmed.startsWith("{")) {
|
|
468
|
+
try {
|
|
469
|
+
const obj = JSON.parse(trimmed);
|
|
470
|
+
if (obj && typeof obj === "object" && obj.error) return true;
|
|
471
|
+
} catch { /* not JSON; fall through */ }
|
|
472
|
+
}
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function errorDetail(res) {
|
|
477
|
+
if (!res) return null;
|
|
478
|
+
if (res.error?.message) return res.error.message;
|
|
479
|
+
if (!res.text) return null;
|
|
480
|
+
try {
|
|
481
|
+
const obj = JSON.parse(res.text);
|
|
482
|
+
if (obj?.error) return obj.detail || obj.error;
|
|
483
|
+
} catch { /* not JSON */ }
|
|
484
|
+
return res.text.slice(0, 200);
|
|
485
|
+
}
|
package/lib/prompt.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { createInterface } from "node:readline";
|
|
10
|
+
import { Writable } from "node:stream";
|
|
10
11
|
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
12
13
|
// Lazy readline singleton
|
|
@@ -155,93 +156,52 @@ export async function askText(questionText, defaultValue) {
|
|
|
155
156
|
// ---------------------------------------------------------------------------
|
|
156
157
|
|
|
157
158
|
/**
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
159
|
+
* Hidden input — uses a muted Writable so readline never echoes typed
|
|
160
|
+
* characters. Matches the behavior of `sudo`, `ssh`, `git`, and `npm login`:
|
|
161
|
+
* the cursor sits in place while you type or paste, then a newline appears
|
|
162
|
+
* on submit. No `*` masking (we tried that for four releases and shipped a
|
|
163
|
+
* security bug each time — readline already handles ANSI sequences,
|
|
164
|
+
* bracketed paste, backspace, arrows, and UTF-8 correctly).
|
|
162
165
|
*
|
|
163
166
|
* @param {string} questionText
|
|
164
167
|
* @returns {Promise<string>}
|
|
165
168
|
*/
|
|
166
169
|
export async function askSecret(questionText) {
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
// with readline over cursor position.
|
|
174
|
-
const pausedRL = rl;
|
|
175
|
-
if (pausedRL) pausedRL.pause();
|
|
170
|
+
// Close the visible singleton so the muted readline gets exclusive stdin.
|
|
171
|
+
// getRL() recreates it cleanly on the next askText/askYesNo call.
|
|
172
|
+
if (rl) {
|
|
173
|
+
rl.close();
|
|
174
|
+
rl = null;
|
|
175
|
+
}
|
|
176
176
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const stdin = process.stdin;
|
|
177
|
+
// Print the prompt through the real stdout so the user sees it.
|
|
178
|
+
process.stdout.write(` ${questionText} `);
|
|
180
179
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
fallbackRL.close();
|
|
188
|
-
if (pausedRL) pausedRL.resume();
|
|
189
|
-
resolve(answer.trim());
|
|
190
|
-
});
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
180
|
+
// Muted Writable: swallows every byte readline tries to echo. The prompt
|
|
181
|
+
// has already been written above; from here on, silence until we print
|
|
182
|
+
// our own newline after Enter.
|
|
183
|
+
const muted = new Writable({
|
|
184
|
+
write(_chunk, _enc, cb) { cb(); },
|
|
185
|
+
});
|
|
193
186
|
|
|
194
|
-
|
|
195
|
-
stdin
|
|
196
|
-
|
|
197
|
-
|
|
187
|
+
const secretRL = createInterface({
|
|
188
|
+
input: process.stdin,
|
|
189
|
+
output: muted,
|
|
190
|
+
terminal: true,
|
|
191
|
+
});
|
|
198
192
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
stdin.removeListener("data", onData);
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
secretRL.once("SIGINT", () => {
|
|
195
|
+
secretRL.close();
|
|
203
196
|
process.stdout.write("\n");
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function onData(ch) {
|
|
208
|
-
const code = ch.charCodeAt(0);
|
|
209
|
-
|
|
210
|
-
// Ctrl-C - abort gracefully
|
|
211
|
-
if (ch === "\u0003") {
|
|
212
|
-
cleanup();
|
|
213
|
-
reject(new Error("User cancelled"));
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Enter / Return
|
|
218
|
-
if (ch === "\r" || ch === "\n") {
|
|
219
|
-
cleanup();
|
|
220
|
-
resolve(secret);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Backspace / Delete (char code 127 or 8)
|
|
225
|
-
if (code === 127 || code === 8) {
|
|
226
|
-
if (secret.length > 0) {
|
|
227
|
-
secret = secret.slice(0, -1);
|
|
228
|
-
// Erase the last asterisk: move cursor back, overwrite, move back.
|
|
229
|
-
process.stdout.write("\b \b");
|
|
230
|
-
}
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Ignore other control characters (arrows, etc.)
|
|
235
|
-
if (code < 32) {
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Normal printable character
|
|
240
|
-
secret += ch;
|
|
241
|
-
process.stdout.write("*");
|
|
242
|
-
}
|
|
197
|
+
reject(new Error("User cancelled"));
|
|
198
|
+
});
|
|
243
199
|
|
|
244
|
-
|
|
200
|
+
secretRL.question("", (answer) => {
|
|
201
|
+
secretRL.close();
|
|
202
|
+
process.stdout.write("\n");
|
|
203
|
+
resolve(answer);
|
|
204
|
+
});
|
|
245
205
|
});
|
|
246
206
|
}
|
|
247
207
|
|
package/lib/validate.mjs
CHANGED
|
@@ -74,7 +74,7 @@ export async function validateConfig(serverName) {
|
|
|
74
74
|
* Attempt an MCP `initialize` handshake against the server URL.
|
|
75
75
|
*
|
|
76
76
|
* @param {string} url - Full MCP endpoint URL (e.g. http://localhost:8000/mcp).
|
|
77
|
-
* @param {string|null} apiKey - Optional API key (sent as `
|
|
77
|
+
* @param {string|null} apiKey - Optional API key (sent as `Authorization: Bearer <key>`).
|
|
78
78
|
* @returns {Promise<{ok: boolean, message: string, sessionId?: string}>}
|
|
79
79
|
*/
|
|
80
80
|
export async function validateConnectivity(url, apiKey) {
|