@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 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) {
@@ -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 = await askChoice('Connection type:', connectionOptions);
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 "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;
@@ -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", { agentId: item.agentId, name: item.name, error: res.error?.message });
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
- * 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
- // Print the prompt without a trailing newline.
168
- process.stdout.write(` ${questionText} `);
169
-
170
- // Pause the singleton readline so its data listener doesn't echo each
171
- // keystroke in parallel with our manual "*" write. Without this, users
172
- // see `d*j*f*k*...` (raw char + mask interleaved) and backspace fights
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
- return new Promise((resolve, reject) => {
178
- let secret = "";
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
- // We need raw mode to capture keystrokes individually.
182
- // Guard: raw mode is only available on TTY streams.
183
- if (!stdin.isTTY) {
184
- // Fallback: non-TTY environments (piped input) - read a plain line.
185
- const fallbackRL = createInterface({ input: stdin, output: process.stdout });
186
- fallbackRL.question("", (answer) => {
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
- const wasRaw = stdin.isRaw;
195
- stdin.setRawMode(true);
196
- stdin.resume();
197
- stdin.setEncoding("utf8");
187
+ const secretRL = createInterface({
188
+ input: process.stdin,
189
+ output: muted,
190
+ terminal: true,
191
+ });
198
192
 
199
- /** Restore stdin state and print a newline after the masked input. */
200
- function cleanup() {
201
- stdin.setRawMode(wasRaw);
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
- if (pausedRL) pausedRL.resume();
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
- stdin.on("data", onData);
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 `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.5",
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": {