@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 +8 -4
- package/assets/hooks-handlers/lib/capture.js +1 -1
- package/bin/create-meko-setup.mjs +5 -8
- 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/prompt.mjs +31 -75
- 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) {
|
|
@@ -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;
|
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
|
-
*
|
|
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
|
-
// 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
|
|
177
|
+
// Print the prompt through the real stdout so the user sees it.
|
|
180
178
|
process.stdout.write(` ${questionText} `);
|
|
181
179
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
199
|
-
stdin
|
|
200
|
-
|
|
201
|
-
|
|
187
|
+
const secretRL = createInterface({
|
|
188
|
+
input: process.stdin,
|
|
189
|
+
output: muted,
|
|
190
|
+
terminal: true,
|
|
191
|
+
});
|
|
202
192
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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 `
|
|
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) {
|