@yugabytedb/meko-mcp 1.0.8 → 1.2.0
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 +242 -44
- package/assets/hooks-handlers/before-submit-prompt.sh +44 -0
- package/assets/hooks-handlers/cursor-session-start.sh +42 -0
- package/assets/hooks-handlers/lib/capture.js +457 -107
- package/assets/hooks-handlers/lib/package.json +1 -0
- package/assets/hooks-handlers/test_single_conversation.sh +1 -1
- package/assets/scripts/package-desktop-skill.sh +36 -0
- package/assets/skills/meko-mcp-tools/SKILL.md +167 -53
- package/assets/skills/meko-mcp-tools/references/_sections.md +1 -1
- package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +84 -22
- package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +89 -56
- package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +15 -7
- package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +17 -9
- package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +94 -5
- package/assets/skills/meko-mcp-tools/references/tools-overview.md +52 -37
- package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +108 -37
- package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +6 -10
- package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +30 -18
- package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +55 -69
- package/assets/skills/meko-mcp-tools-desktop/SKILL.md +251 -0
- package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +150 -0
- package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +24 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +99 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +406 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +69 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +217 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +112 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +134 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-scope-permissions.md +75 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +155 -0
- package/bin/create-meko-setup.mjs +151 -22
- package/lib/clients/claude-code.mjs +378 -68
- package/lib/clients/claude-desktop.mjs +45 -8
- package/lib/clients/codex.mjs +1209 -0
- package/lib/clients/common.mjs +223 -1
- package/lib/clients/cursor.mjs +118 -13
- package/lib/clients/index.mjs +2 -0
- package/lib/config.mjs +56 -0
- package/lib/detect.mjs +52 -44
- package/lib/installer.mjs +11 -4
- package/lib/mcp-transport.mjs +61 -2
- package/lib/migrate/adapters/memory-md.mjs +13 -6
- package/lib/migrate/datapack.mjs +167 -0
- package/lib/migrate/id.mjs +127 -9
- package/lib/migrate/orchestrator.mjs +68 -10
- package/lib/migrate.mjs +346 -3
- package/lib/prompt.mjs +22 -4
- package/lib/validate.mjs +313 -9
- package/lib/version-check.mjs +178 -0
- package/package.json +2 -1
- package/scripts/bundle-assets.mjs +8 -0
package/lib/validate.mjs
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* validate.mjs - Post-install validation for Meko MCP Server.
|
|
3
3
|
*
|
|
4
|
-
* Runs
|
|
4
|
+
* Runs four checks in sequence:
|
|
5
5
|
* 1. Config check - Is the server registered in `claude mcp list`?
|
|
6
|
-
* 2. Connectivity
|
|
7
|
-
* 3. Tools
|
|
6
|
+
* 2. Connectivity - Can we complete an MCP `initialize` handshake?
|
|
7
|
+
* 3. Tools - Does `tools/list` return the expected core tools?
|
|
8
|
+
* 4. Canary - Does a deterministic read-only Meko tool call return
|
|
9
|
+
* a sane response? Proves auth → tenant → datapack
|
|
10
|
+
* routing end-to-end without depending on mem0's
|
|
11
|
+
* LLM fact-extractor (which historically failed the
|
|
12
|
+
* canary non-deterministically).
|
|
8
13
|
*
|
|
9
14
|
* HTTP transport lives in ./mcp-transport.mjs and is shared with the
|
|
10
15
|
* migrate subcommand.
|
|
@@ -25,10 +30,18 @@ import {
|
|
|
25
30
|
/** Core tools that must appear in tools/list for validation to pass. */
|
|
26
31
|
const CORE_TOOLS = [
|
|
27
32
|
"memory_search",
|
|
33
|
+
"memory_get_by_id",
|
|
28
34
|
"conversation_create",
|
|
29
35
|
"db_summarize_database",
|
|
36
|
+
"datapack_list",
|
|
30
37
|
];
|
|
31
38
|
|
|
39
|
+
/** Nil UUID used as the conversation_id placeholder for the canary call. */
|
|
40
|
+
const NIL_UUID = "00000000-0000-0000-0000-000000000000";
|
|
41
|
+
|
|
42
|
+
/** Tool the canary invokes. */
|
|
43
|
+
const CANARY_TOOL = "datapack_list";
|
|
44
|
+
|
|
32
45
|
// ---------------------------------------------------------------------------
|
|
33
46
|
// 1. Config validation
|
|
34
47
|
// ---------------------------------------------------------------------------
|
|
@@ -118,21 +131,149 @@ export async function validateTools(url, apiKey) {
|
|
|
118
131
|
}
|
|
119
132
|
|
|
120
133
|
// ---------------------------------------------------------------------------
|
|
121
|
-
// 4.
|
|
134
|
+
// 4. Canary validation (deterministic, read-only)
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Call a deterministic, read-only Meko tool and verify the response shape.
|
|
139
|
+
*
|
|
140
|
+
* The canary's job is to prove the part of the stack that connectivity +
|
|
141
|
+
* tools/list don't cover: auth was accepted as a real Meko identity, the
|
|
142
|
+
* tenant resolver mapped it to an account, and the datapack routing wired
|
|
143
|
+
* up the user's actual workspace. We invoke `datapack_list` for this — it
|
|
144
|
+
* is a single read-only call, runs a short SQL query against the system
|
|
145
|
+
* schema, and returns deterministically (every authenticated user has at
|
|
146
|
+
* least one datapack: their default).
|
|
147
|
+
*
|
|
148
|
+
* Why not memory_add? The previous canary did a write/read/delete round-trip
|
|
149
|
+
* via `memory_add`/`memory_get_by_id`/`memory_delete_by_id`. mem0's LLM
|
|
150
|
+
* fact-extractor decides per-call whether the input contains facts worth
|
|
151
|
+
* indexing; for the canary text it returned `results: []` *some of the time*
|
|
152
|
+
* even though the graph extractor did add entities. The installer's id
|
|
153
|
+
* extraction then reported "no memory id" and failed validation despite the
|
|
154
|
+
* server being healthy. A non-LLM read-only tool removes that whole class
|
|
155
|
+
* of false negative.
|
|
156
|
+
*
|
|
157
|
+
* Security note: `datapack_list` returns each datapack's
|
|
158
|
+
* `datapack_ysql_connection_string`, which contains a database password.
|
|
159
|
+
* This function MUST NOT echo the raw response into a user-facing message.
|
|
160
|
+
* Failure messages reference the tool name, the JSON-RPC error code, or a
|
|
161
|
+
* server-emitted `error` discriminator — never the body.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} url - Full MCP endpoint URL.
|
|
164
|
+
* @param {string|null} apiKey - Optional API key.
|
|
165
|
+
* @param {object} [options]
|
|
166
|
+
* @param {string} [options.sessionId] - Existing MCP session id. If omitted, a new
|
|
167
|
+
* initialize handshake is performed.
|
|
168
|
+
* @param {number} [options.timeoutMs] - Override the canary call's transport timeout.
|
|
169
|
+
* Defaults to DEFAULT_REQUEST_TIMEOUT_MS.
|
|
170
|
+
* @returns {Promise<{
|
|
171
|
+
* ok: boolean,
|
|
172
|
+
* message: string,
|
|
173
|
+
* datapack_count: number|null,
|
|
174
|
+
* step?: "initialize"|"datapack_list",
|
|
175
|
+
* }>}
|
|
176
|
+
*/
|
|
177
|
+
export async function validateCanary(url, apiKey, options = {}) {
|
|
178
|
+
const timeoutMs = options.timeoutMs;
|
|
179
|
+
|
|
180
|
+
let sessionId = options.sessionId ?? null;
|
|
181
|
+
if (!sessionId) {
|
|
182
|
+
const init = await mcpInitialize(url, apiKey);
|
|
183
|
+
if (!init.ok) {
|
|
184
|
+
return {
|
|
185
|
+
ok: false,
|
|
186
|
+
datapack_count: null,
|
|
187
|
+
step: "initialize",
|
|
188
|
+
message: `Canary: could not initialize MCP session (${init.message}).`,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
sessionId = init.sessionId;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const callRes = await mcpCallTool(
|
|
195
|
+
url,
|
|
196
|
+
apiKey,
|
|
197
|
+
sessionId,
|
|
198
|
+
CANARY_TOOL,
|
|
199
|
+
{
|
|
200
|
+
scope: "read",
|
|
201
|
+
conversation_id: NIL_UUID,
|
|
202
|
+
},
|
|
203
|
+
{ timeoutMs },
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
if (!callRes.ok) {
|
|
207
|
+
// mcpCallTool already produces a sanitized message (tool name + reason
|
|
208
|
+
// or JSON-RPC error code); it never includes the raw text body when
|
|
209
|
+
// the server emits a structured `{error: ...}` envelope. Pass it
|
|
210
|
+
// through verbatim.
|
|
211
|
+
return {
|
|
212
|
+
ok: false,
|
|
213
|
+
datapack_count: null,
|
|
214
|
+
step: CANARY_TOOL,
|
|
215
|
+
message: `Canary: ${callRes.message}`,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const datapacks = parseDatapackList(callRes.text);
|
|
220
|
+
if (!datapacks) {
|
|
221
|
+
// The tool returned a 2xx but the body wasn't a recognizable list.
|
|
222
|
+
// Don't dump it — list_datapacks responses include connection-string
|
|
223
|
+
// secrets when they parse correctly, and we have no way to know whether
|
|
224
|
+
// the unparseable body has the same.
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
datapack_count: null,
|
|
228
|
+
step: CANARY_TOOL,
|
|
229
|
+
message:
|
|
230
|
+
`Canary: ${CANARY_TOOL} returned an unexpected response shape ` +
|
|
231
|
+
`(expected a JSON array of datapack objects). Check the server logs.`,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (datapacks.length === 0) {
|
|
236
|
+
return {
|
|
237
|
+
ok: false,
|
|
238
|
+
datapack_count: 0,
|
|
239
|
+
step: CANARY_TOOL,
|
|
240
|
+
message:
|
|
241
|
+
`Canary: ${CANARY_TOOL} returned zero datapacks. Every authenticated ` +
|
|
242
|
+
`Meko user should have at least their default datapack — this likely ` +
|
|
243
|
+
`means the API token authenticated but resolved to the wrong tenant. ` +
|
|
244
|
+
`Verify the API key matches the tenant the URL points at.`,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
ok: true,
|
|
250
|
+
datapack_count: datapacks.length,
|
|
251
|
+
message: `Canary OK — ${CANARY_TOOL} returned ${datapacks.length} datapack(s).`,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// 5. Combined runner
|
|
122
257
|
// ---------------------------------------------------------------------------
|
|
123
258
|
|
|
124
259
|
/**
|
|
125
260
|
* Run all validation checks in sequence and return a summary.
|
|
126
261
|
*
|
|
127
262
|
* When the connectivity check succeeds, the tools check reuses the same
|
|
128
|
-
* MCP session rather than performing a redundant second initialize.
|
|
263
|
+
* MCP session rather than performing a redundant second initialize. The
|
|
264
|
+
* canary reuses that same session too.
|
|
265
|
+
*
|
|
266
|
+
* The canary is skipped (not failed) when tools/list didn't pass — the
|
|
267
|
+
* canary depends on `datapack_list` being present, and a tools failure
|
|
268
|
+
* already surfaces that.
|
|
129
269
|
*
|
|
130
270
|
* @param {string} url - Full MCP endpoint URL.
|
|
131
271
|
* @param {string|null} apiKey - Optional API key.
|
|
132
272
|
* @param {string} serverName - Server name for config check (default "meko").
|
|
133
273
|
* @param {object} [options]
|
|
134
274
|
* @param {(params: {serverName: string}) => Promise<{ok: boolean, message: string}>} [options.validateConfig]
|
|
135
|
-
* @
|
|
275
|
+
* @param {boolean} [options.skipCanary] - Skip the canary entirely.
|
|
276
|
+
* @returns {Promise<{config: object, connectivity: object, tools: object, canary: object|null, allPassed: boolean}>}
|
|
136
277
|
*/
|
|
137
278
|
export async function runValidation(url, apiKey, serverName = "meko", options = {}) {
|
|
138
279
|
const configValidator = options.validateConfig ?? (async ({ serverName: value }) => validateConfig(value));
|
|
@@ -147,11 +288,34 @@ export async function runValidation(url, apiKey, serverName = "meko", options =
|
|
|
147
288
|
tools = { ok: false, tools: [], message: `Skipped: ${connectivity.message}` };
|
|
148
289
|
}
|
|
149
290
|
|
|
291
|
+
let canary = null;
|
|
292
|
+
if (!options.skipCanary) {
|
|
293
|
+
if (tools.ok && connectivity.ok) {
|
|
294
|
+
canary = await validateCanary(url, apiKey, {
|
|
295
|
+
sessionId: connectivity.sessionId,
|
|
296
|
+
});
|
|
297
|
+
} else {
|
|
298
|
+
canary = {
|
|
299
|
+
ok: false,
|
|
300
|
+
datapack_count: null,
|
|
301
|
+
message: `Skipped: tools/list did not pass (${tools.message}).`,
|
|
302
|
+
skipped: true,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const allPassed =
|
|
308
|
+
config.ok &&
|
|
309
|
+
connectivity.ok &&
|
|
310
|
+
tools.ok &&
|
|
311
|
+
(!canary || canary.ok);
|
|
312
|
+
|
|
150
313
|
return {
|
|
151
314
|
config,
|
|
152
315
|
connectivity,
|
|
153
316
|
tools,
|
|
154
|
-
|
|
317
|
+
canary,
|
|
318
|
+
allPassed,
|
|
155
319
|
};
|
|
156
320
|
}
|
|
157
321
|
|
|
@@ -190,7 +354,7 @@ async function sendToolsList(url, apiKey, sessionId) {
|
|
|
190
354
|
return {
|
|
191
355
|
ok: false,
|
|
192
356
|
tools: [],
|
|
193
|
-
message: `tools/list returned HTTP ${res.statusCode}: ${res.body}`,
|
|
357
|
+
message: `tools/list returned HTTP ${res.statusCode}: ${String(res.body ?? "").slice(0, 300)}`,
|
|
194
358
|
};
|
|
195
359
|
}
|
|
196
360
|
|
|
@@ -201,7 +365,7 @@ async function sendToolsList(url, apiKey, sessionId) {
|
|
|
201
365
|
return {
|
|
202
366
|
ok: false,
|
|
203
367
|
tools: [],
|
|
204
|
-
message: `tools/list returned non-JSON: ${res.body.slice(0, 200)}`,
|
|
368
|
+
message: `tools/list returned non-JSON: ${String(res.body ?? "").slice(0, 200)}`,
|
|
205
369
|
};
|
|
206
370
|
}
|
|
207
371
|
|
|
@@ -234,6 +398,146 @@ async function sendToolsList(url, apiKey, sessionId) {
|
|
|
234
398
|
};
|
|
235
399
|
}
|
|
236
400
|
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
// Internal: tools/call for the canary
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Send a `tools/call` request and unwrap the MCP content envelope.
|
|
407
|
+
*
|
|
408
|
+
* Returns `{ok, text, message}` where `text` is the first text block (what
|
|
409
|
+
* Meko tools return as a JSON-encoded string) and `message` is only
|
|
410
|
+
* populated on the non-ok path. On the non-ok path the message is sanitized
|
|
411
|
+
* — for tool-level errors emitted as `{"error": "...", "detail": "..."}`,
|
|
412
|
+
* the discriminator and detail are surfaced; the raw body is never
|
|
413
|
+
* echoed because it can contain secrets (e.g. `datapack_list` returns
|
|
414
|
+
* connection strings).
|
|
415
|
+
*
|
|
416
|
+
* @param {string} url
|
|
417
|
+
* @param {string|null} apiKey
|
|
418
|
+
* @param {string|null} sessionId
|
|
419
|
+
* @param {string} name - Tool name, e.g. "datapack_list".
|
|
420
|
+
* @param {object} args - Tool arguments.
|
|
421
|
+
* @param {{timeoutMs?: number}} [opts]
|
|
422
|
+
* @returns {Promise<{ok: boolean, text: string|null, message?: string}>}
|
|
423
|
+
*/
|
|
424
|
+
async function mcpCallTool(url, apiKey, sessionId, name, args, opts = {}) {
|
|
425
|
+
const payload = {
|
|
426
|
+
jsonrpc: "2.0",
|
|
427
|
+
id: Date.now() + Math.floor(Math.random() * 1000),
|
|
428
|
+
method: "tools/call",
|
|
429
|
+
params: { name, arguments: args },
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const timeoutMs = opts.timeoutMs;
|
|
433
|
+
let res;
|
|
434
|
+
try {
|
|
435
|
+
res = await jsonRpcRequest(url, payload, apiKey, sessionId, { timeoutMs });
|
|
436
|
+
} catch (err) {
|
|
437
|
+
return {
|
|
438
|
+
ok: false,
|
|
439
|
+
text: null,
|
|
440
|
+
message: `${name} request failed: ${classifyError(err, timeoutMs)}`,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
445
|
+
return {
|
|
446
|
+
ok: false,
|
|
447
|
+
text: null,
|
|
448
|
+
message: `${name} returned HTTP ${res.statusCode}: ${String(res.body ?? "").slice(0, 300)}`,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
let parsed;
|
|
453
|
+
try {
|
|
454
|
+
parsed = JSON.parse(res.body);
|
|
455
|
+
} catch {
|
|
456
|
+
return {
|
|
457
|
+
ok: false,
|
|
458
|
+
text: null,
|
|
459
|
+
message: `${name} returned non-JSON at the JSON-RPC envelope.`,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (parsed.error) {
|
|
464
|
+
return {
|
|
465
|
+
ok: false,
|
|
466
|
+
text: null,
|
|
467
|
+
message: `${name} error: ${parsed.error.message ?? JSON.stringify(parsed.error)}`,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const text = extractFirstText(parsed.result);
|
|
472
|
+
|
|
473
|
+
// Meko tools sometimes encode a tool-level failure inside the text body
|
|
474
|
+
// (e.g. memory_add returning `{"error": "memory_add_failed", ...}`,
|
|
475
|
+
// datapack_list returning `{"error": "list_failed", "detail": ...}`).
|
|
476
|
+
// Surface the discriminator and detail — never the full body, since the
|
|
477
|
+
// success body of datapack_list contains connection-string secrets and we
|
|
478
|
+
// can't distinguish before parsing.
|
|
479
|
+
if (text) {
|
|
480
|
+
try {
|
|
481
|
+
const body = JSON.parse(text);
|
|
482
|
+
if (body && typeof body === "object" && !Array.isArray(body) && typeof body.error === "string") {
|
|
483
|
+
const detail = typeof body.detail === "string" ? ` (${body.detail})` : "";
|
|
484
|
+
return {
|
|
485
|
+
ok: false,
|
|
486
|
+
text,
|
|
487
|
+
message: `${name} server error: ${body.error}${detail}`,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
} catch {
|
|
491
|
+
// Non-JSON bodies are fine — many tools return plain text.
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return { ok: true, text };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Meko tool results come back as `{content: [{type:"text", text: "..."}]}`.
|
|
500
|
+
*/
|
|
501
|
+
function extractFirstText(result) {
|
|
502
|
+
const content = result?.content;
|
|
503
|
+
if (!Array.isArray(content)) return null;
|
|
504
|
+
for (const block of content) {
|
|
505
|
+
if (block && block.type === "text" && typeof block.text === "string") {
|
|
506
|
+
return block.text;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Parse a `datapack_list` response body and return the array of datapacks,
|
|
514
|
+
* or `null` if the body is not a recognizable datapack list.
|
|
515
|
+
*
|
|
516
|
+
* Successful `datapack_list` returns a JSON array; each element has at
|
|
517
|
+
* minimum a `datapack_id` and `datapack_name`. We accept any array whose
|
|
518
|
+
* elements look like datapack records — we deliberately don't enumerate or
|
|
519
|
+
* log the elements since they include credentials.
|
|
520
|
+
*
|
|
521
|
+
* @param {string|null} text
|
|
522
|
+
* @returns {Array|null}
|
|
523
|
+
*/
|
|
524
|
+
function parseDatapackList(text) {
|
|
525
|
+
if (!text) return null;
|
|
526
|
+
let body;
|
|
527
|
+
try {
|
|
528
|
+
body = JSON.parse(text);
|
|
529
|
+
} catch {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
if (!Array.isArray(body)) return null;
|
|
533
|
+
// Empty array is a meaningful answer (caller treats it as "wrong tenant").
|
|
534
|
+
if (body.length === 0) return body;
|
|
535
|
+
const allLookLikeDatapacks = body.every(
|
|
536
|
+
(entry) => entry && typeof entry === "object" && typeof entry.datapack_id === "string",
|
|
537
|
+
);
|
|
538
|
+
return allLookLikeDatapacks ? body : null;
|
|
539
|
+
}
|
|
540
|
+
|
|
237
541
|
// ---------------------------------------------------------------------------
|
|
238
542
|
// Internal: child process helper
|
|
239
543
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* version-check.mjs — Optional npm-registry version probe.
|
|
3
|
+
*
|
|
4
|
+
* Never throws, never rejects, never blocks the install. Used by the installer
|
|
5
|
+
* to surface an upgrade hint when a newer @yugabytedb/meko-mcp is available.
|
|
6
|
+
*
|
|
7
|
+
* Zero external dependencies (node:https + node:url only), matching the
|
|
8
|
+
* Design Principles documented in installer/README.md.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import https from "node:https";
|
|
12
|
+
import { URL } from "node:url";
|
|
13
|
+
|
|
14
|
+
const REGISTRY_URL =
|
|
15
|
+
"https://registry.npmjs.org/@yugabytedb/meko-mcp/latest";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Semver-ish comparator. Returns -1 if a<b, 0 if equal, 1 if a>b, or `null`
|
|
19
|
+
* when the comparison cannot be ordered (e.g. a side contains non-numeric
|
|
20
|
+
* pre-release tags).
|
|
21
|
+
*
|
|
22
|
+
* Deliberately minimal: splits on ".", maps to Number, and compares
|
|
23
|
+
* lexicographically over the resulting tuple. Does NOT order pre-release
|
|
24
|
+
* tags (`1.0.0-alpha.1`) — for the installer we only care about stable
|
|
25
|
+
* releases like 1.0.8 vs 1.0.9. The `null` sentinel lets callers
|
|
26
|
+
* distinguish "equal" (0) from "incomparable" and fall back to strict
|
|
27
|
+
* string equality only when actually needed, so identical-but-formatted-
|
|
28
|
+
* differently versions like `1.0` vs `1.0.0` are NOT flagged as outdated.
|
|
29
|
+
*/
|
|
30
|
+
export function compareSemver(a, b) {
|
|
31
|
+
const partsA = String(a).split(".").map((x) => Number(x));
|
|
32
|
+
const partsB = String(b).split(".").map((x) => Number(x));
|
|
33
|
+
const hasNaN = partsA.some((n) => Number.isNaN(n)) || partsB.some((n) => Number.isNaN(n));
|
|
34
|
+
if (hasNaN) {
|
|
35
|
+
return a === b ? 0 : null;
|
|
36
|
+
}
|
|
37
|
+
const len = Math.max(partsA.length, partsB.length);
|
|
38
|
+
for (let i = 0; i < len; i++) {
|
|
39
|
+
const x = partsA[i] ?? 0;
|
|
40
|
+
const y = partsB[i] ?? 0;
|
|
41
|
+
if (x < y) return -1;
|
|
42
|
+
if (x > y) return 1;
|
|
43
|
+
}
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Fetch the latest published version of @yugabytedb/meko-mcp from the npm
|
|
49
|
+
* registry and compare against `currentVersion`.
|
|
50
|
+
*
|
|
51
|
+
* Never throws. On any network error, timeout, or parse failure, returns
|
|
52
|
+
* `{ current, latest: null, isOutdated: false, error: <string> }`.
|
|
53
|
+
*
|
|
54
|
+
* @param {object} opts
|
|
55
|
+
* @param {string} opts.currentVersion — the installed version to compare against.
|
|
56
|
+
* @param {number} [opts.timeoutMs=2000] — hard cap on the registry request.
|
|
57
|
+
* @param {boolean} [opts.background=false] — when true, unref the request's
|
|
58
|
+
* underlying socket so an in-flight probe does not keep the Node process
|
|
59
|
+
* alive after the main install completes. Callers who `await` the result
|
|
60
|
+
* (e.g. the `--check-updates` handler) should leave this false.
|
|
61
|
+
* @returns {Promise<{current: string, latest: string|null, isOutdated: boolean, error?: string}>}
|
|
62
|
+
*/
|
|
63
|
+
export async function checkLatest({ currentVersion, timeoutMs = 2000, background = false, _requestImpl } = {}) {
|
|
64
|
+
const requestImpl = _requestImpl ?? https.request;
|
|
65
|
+
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
let settled = false;
|
|
68
|
+
const done = (result) => {
|
|
69
|
+
if (settled) return;
|
|
70
|
+
settled = true;
|
|
71
|
+
resolve({ current: currentVersion, ...result });
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
let url;
|
|
75
|
+
try {
|
|
76
|
+
url = new URL(REGISTRY_URL);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
return done({ latest: null, isOutdated: false, error: `bad-url: ${err.message}` });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let req;
|
|
82
|
+
try {
|
|
83
|
+
req = requestImpl(
|
|
84
|
+
{
|
|
85
|
+
method: "GET",
|
|
86
|
+
hostname: url.hostname,
|
|
87
|
+
port: url.port || 443,
|
|
88
|
+
path: url.pathname + url.search,
|
|
89
|
+
headers: {
|
|
90
|
+
Accept: "application/json",
|
|
91
|
+
"User-Agent": `@yugabytedb/meko-mcp/${currentVersion}`,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
(res) => {
|
|
95
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
96
|
+
// Drain so the socket can be freed, then report error.
|
|
97
|
+
res.resume();
|
|
98
|
+
return done({
|
|
99
|
+
latest: null,
|
|
100
|
+
isOutdated: false,
|
|
101
|
+
error: `http ${res.statusCode}`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
let body = "";
|
|
105
|
+
res.setEncoding("utf8");
|
|
106
|
+
res.on("data", (chunk) => (body += chunk));
|
|
107
|
+
res.on("end", () => {
|
|
108
|
+
try {
|
|
109
|
+
const parsed = JSON.parse(body);
|
|
110
|
+
const latest = parsed?.version;
|
|
111
|
+
if (typeof latest !== "string" || latest.length === 0) {
|
|
112
|
+
return done({
|
|
113
|
+
latest: null,
|
|
114
|
+
isOutdated: false,
|
|
115
|
+
error: "registry response missing .version",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
let isOutdated;
|
|
119
|
+
const cmp = compareSemver(currentVersion, latest);
|
|
120
|
+
if (cmp === null) {
|
|
121
|
+
// Incomparable (non-numeric tags). Fall back to strict
|
|
122
|
+
// string inequality; only flag outdated if they actually
|
|
123
|
+
// differ as strings.
|
|
124
|
+
isOutdated = currentVersion !== latest;
|
|
125
|
+
} else {
|
|
126
|
+
isOutdated = cmp < 0;
|
|
127
|
+
}
|
|
128
|
+
done({ latest, isOutdated });
|
|
129
|
+
} catch (err) {
|
|
130
|
+
done({
|
|
131
|
+
latest: null,
|
|
132
|
+
isOutdated: false,
|
|
133
|
+
error: `parse: ${err.message}`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
res.on("error", (err) => {
|
|
138
|
+
done({ latest: null, isOutdated: false, error: err.message });
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
return done({ latest: null, isOutdated: false, error: err.message });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const timer = setTimeout(() => {
|
|
147
|
+
try {
|
|
148
|
+
req.destroy(new Error("timeout"));
|
|
149
|
+
} catch {
|
|
150
|
+
/* ignore */
|
|
151
|
+
}
|
|
152
|
+
done({ latest: null, isOutdated: false, error: "timeout" });
|
|
153
|
+
}, timeoutMs);
|
|
154
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
155
|
+
|
|
156
|
+
// In background mode, unref the socket so Node can exit as soon as the
|
|
157
|
+
// main work finishes even if the registry request is still in flight.
|
|
158
|
+
// The socket appears on "socket" — not on the request object itself at
|
|
159
|
+
// construction time — so we subscribe rather than reaching for req.socket.
|
|
160
|
+
if (background) {
|
|
161
|
+
req.on("socket", (socket) => {
|
|
162
|
+
if (socket && typeof socket.unref === "function") socket.unref();
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
req.on("error", (err) => {
|
|
167
|
+
clearTimeout(timer);
|
|
168
|
+
done({ latest: null, isOutdated: false, error: err.message });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
req.end();
|
|
173
|
+
} catch (err) {
|
|
174
|
+
clearTimeout(timer);
|
|
175
|
+
done({ latest: null, isOutdated: false, error: err.message });
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yugabytedb/meko-mcp",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Single-line installer for Meko MCP Server across Claude and Cursor clients",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"postpack": "node scripts/bundle-assets.mjs --clean",
|
|
20
20
|
"bundle-assets": "node scripts/bundle-assets.mjs",
|
|
21
21
|
"test": "node --test \"test/*.mjs\"",
|
|
22
|
+
"smoke": "./scripts/prepublish-smoke.sh",
|
|
22
23
|
"test:unit": "node --test test/config.test.mjs test/validate.test.mjs test/clients.test.mjs",
|
|
23
24
|
"test:integration": "RUN_INSTALLER_INTEGRATION=1 node --test test/installer.test.mjs",
|
|
24
25
|
"test:integration:cloud": "RUN_INSTALLER_INTEGRATION=1 node --test test/installer.test.mjs"
|
|
@@ -18,10 +18,18 @@ const PAYLOAD = [
|
|
|
18
18
|
from: join(repoRoot, "skills", "skills", "meko-mcp-tools"),
|
|
19
19
|
to: join(assetsDir, "skills", "meko-mcp-tools"),
|
|
20
20
|
},
|
|
21
|
+
{
|
|
22
|
+
from: join(repoRoot, "skills", "skills", "meko-mcp-tools-desktop"),
|
|
23
|
+
to: join(assetsDir, "skills", "meko-mcp-tools-desktop"),
|
|
24
|
+
},
|
|
21
25
|
{
|
|
22
26
|
from: join(repoRoot, "skills", "hooks-handlers"),
|
|
23
27
|
to: join(assetsDir, "hooks-handlers"),
|
|
24
28
|
},
|
|
29
|
+
{
|
|
30
|
+
from: join(repoRoot, "skills", "scripts", "package-desktop-skill.sh"),
|
|
31
|
+
to: join(assetsDir, "scripts", "package-desktop-skill.sh"),
|
|
32
|
+
},
|
|
25
33
|
];
|
|
26
34
|
|
|
27
35
|
const cleaning = process.argv.includes("--clean");
|