@yugabytedb/meko-mcp 1.0.6 → 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 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 "API_KEY: <key>"` for cloud) |
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 --sparse .claude-plugin,skills --scope user` |
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 "API_KEY: <key>"` header | No auth header |
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 API_KEY header, env vars set, validation passes |
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["API_KEY"] = MEKO_API_KEY;
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
- const trimmed = (raw ?? '').trim();
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) {
@@ -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 "API_KEY: ${apiKey}"`;
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 "API_KEY: ${redactKey(apiKey)}"`;
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 --sparse .claude-plugin,skills --scope ${scope}`;
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", "API_KEY:${MEKO_API_KEY}"],
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 — some clients mis-parse
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
- // `API_KEY:${MEKO_API_KEY}` with no space around the colon — mcp-remote
232
- // docs call out a spacing bug in some clients' arg parsing. The header
233
- // name matches what the Meko server expects (see capture.js mcpPost).
234
- args.push("--header", "API_KEY:${MEKO_API_KEY}");
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;
@@ -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.API_KEY = <set>");
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 = { API_KEY: apiKey };
95
+ server.headers = { Authorization: `Bearer ${apiKey}` };
96
96
  }
97
97
  cfg.mcpServers[name] = server;
98
98
  writeJsonFile(mcpPath, cfg);
@@ -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 `API_KEY` header.
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["API_KEY"] = apiKey;
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 API_KEY.`,
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;
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,97 +156,52 @@ export async function askText(questionText, defaultValue) {
155
156
  // ---------------------------------------------------------------------------
156
157
 
157
158
  /**
158
- * Masked input that echoes `*` for each character typed.
159
- *
160
- * Puts stdin into raw mode so we can intercept individual keystrokes.
161
- * Handles backspace, Ctrl-C (abort), and Enter (submit).
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
- // Fully close the singleton readline BEFORE printing the prompt or
168
- // entering raw mode. pause() alone was insufficient — readline left
169
- // stdin in a state where the terminal echo would still leak keystrokes
170
- // before our raw-mode handler wrote its `*` mask. close() forcefully
171
- // releases stdin back to the OS, which lets setRawMode actually
172
- // suppress echo. getRL() will recreate the interface on the next
173
- // askText/askYesNo call, so downstream prompts still work.
170
+ // Close the visible singleton so the muted readline gets exclusive stdin.
171
+ // getRL() recreates it cleanly on the next askText/askYesNo call.
174
172
  if (rl) {
175
173
  rl.close();
176
174
  rl = null;
177
175
  }
178
176
 
179
- // Print the prompt without a trailing newline.
177
+ // Print the prompt through the real stdout so the user sees it.
180
178
  process.stdout.write(` ${questionText} `);
181
179
 
182
- return new Promise((resolve, reject) => {
183
- let secret = "";
184
- const stdin = process.stdin;
185
-
186
- // We need raw mode to capture keystrokes individually.
187
- // Guard: raw mode is only available on TTY streams.
188
- if (!stdin.isTTY) {
189
- // Fallback: non-TTY environments (piped input) - read a plain line.
190
- const fallbackRL = createInterface({ input: stdin, output: process.stdout });
191
- fallbackRL.question("", (answer) => {
192
- fallbackRL.close();
193
- resolve(answer.trim());
194
- });
195
- return;
196
- }
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
+ });
197
186
 
198
- const wasRaw = stdin.isRaw;
199
- stdin.setRawMode(true);
200
- stdin.resume();
201
- stdin.setEncoding("utf8");
187
+ const secretRL = createInterface({
188
+ input: process.stdin,
189
+ output: muted,
190
+ terminal: true,
191
+ });
202
192
 
203
- /** Restore stdin state and print a newline after the masked input. */
204
- function cleanup() {
205
- stdin.setRawMode(wasRaw);
206
- stdin.removeListener("data", onData);
207
- stdin.pause();
193
+ return new Promise((resolve, reject) => {
194
+ secretRL.once("SIGINT", () => {
195
+ secretRL.close();
208
196
  process.stdout.write("\n");
209
- }
210
-
211
- function onData(ch) {
212
- const code = ch.charCodeAt(0);
213
-
214
- // Ctrl-C - abort gracefully
215
- if (ch === "\u0003") {
216
- cleanup();
217
- reject(new Error("User cancelled"));
218
- return;
219
- }
220
-
221
- // Enter / Return
222
- if (ch === "\r" || ch === "\n") {
223
- cleanup();
224
- resolve(secret);
225
- return;
226
- }
227
-
228
- // Backspace / Delete (char code 127 or 8)
229
- if (code === 127 || code === 8) {
230
- if (secret.length > 0) {
231
- secret = secret.slice(0, -1);
232
- // Erase the last asterisk: move cursor back, overwrite, move back.
233
- process.stdout.write("\b \b");
234
- }
235
- return;
236
- }
237
-
238
- // Ignore other control characters (arrows, etc.)
239
- if (code < 32) {
240
- return;
241
- }
242
-
243
- // Normal printable character
244
- secret += ch;
245
- process.stdout.write("*");
246
- }
197
+ reject(new Error("User cancelled"));
198
+ });
247
199
 
248
- stdin.on("data", onData);
200
+ secretRL.question("", (answer) => {
201
+ secretRL.close();
202
+ process.stdout.write("\n");
203
+ resolve(answer);
204
+ });
249
205
  });
250
206
  }
251
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 `API_KEY` header).
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yugabytedb/meko-mcp",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Single-line installer for Meko MCP Server across Claude and Cursor clients",
5
5
  "type": "module",
6
6
  "bin": {