@yugabytedb/meko-mcp 2.3.0 → 2.5.1
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 +88 -35
- package/assets/hooks-handlers/lib/capture-cleanup-cli.js +735 -74
- package/assets/hooks-handlers/lib/capture-notices.js +94 -1
- package/assets/hooks-handlers/lib/capture-state.js +111 -0
- package/assets/hooks-handlers/lib/capture.js +130 -4
- package/assets/hooks-handlers/lib/checkpoint-timer.js +394 -8
- package/assets/hooks-handlers/lib/opencode-handler.js +29 -6
- package/assets/scripts/meko-statusline.sh +17 -1
- package/assets/skills/meko-mcp-tools/SKILL.md +6 -5
- package/assets/skills/meko-mcp-tools/references/_sections.md +1 -1
- package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +194 -10
- package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +6 -3
- package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +11 -6
- package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +4 -4
- package/assets/skills/meko-mcp-tools/references/tools-overview.md +19 -14
- package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +69 -8
- package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +3 -1
- package/assets/skills/meko-mcp-tools-desktop/SKILL.md +8 -7
- package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +4 -4
- package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +1 -1
- package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +194 -10
- package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +10 -5
- package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +4 -4
- package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +19 -14
- package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +68 -8
- package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +3 -1
- package/assets/skills/meko-select-datapack-desktop/SKILL.md +91 -12
- package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +39 -7
- package/assets/skills/meko-select-datapack-desktop/references/role-display-future.md +8 -6
- package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +13 -1
- package/bin/create-meko-setup.mjs +152 -35
- package/lib/autofix.mjs +100 -0
- package/lib/browser-login.mjs +495 -0
- package/lib/clients/codex.mjs +1 -24
- package/lib/clients/common.mjs +25 -0
- package/lib/clients/opencode.mjs +6 -1
- package/lib/migrate/datapack.mjs +254 -16
- package/lib/migrate/mcp-client.mjs +34 -0
- package/lib/migrate/orchestrator.mjs +1 -1
- package/lib/migrate.mjs +2 -6
- package/lib/validate.mjs +111 -22
- package/package.json +5 -5
package/lib/migrate/datapack.mjs
CHANGED
|
@@ -8,11 +8,19 @@
|
|
|
8
8
|
* confirmation of the default-fallback case. This module owns the lookup.
|
|
9
9
|
*
|
|
10
10
|
* Wire model: the MCP server routes by `datapack_id` parameter, not URL host.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* We resolve here, then the orchestrator threads `datapack_id` into every
|
|
12
|
+
* subsequent `tools/call`.
|
|
13
|
+
*
|
|
14
|
+
* Lookups go through `datapack_search(name=...)` and `datapack_describe`, so
|
|
15
|
+
* they cost the same for an account with 1M datapacks as for one with two.
|
|
16
|
+
* A server whose tools/list has no `datapack_search` predates it; there we
|
|
17
|
+
* fall back to scanning `datapack_list`, which is fine for the account sizes
|
|
18
|
+
* such a server can serve.
|
|
13
19
|
*/
|
|
14
20
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
15
21
|
const DEFAULT_DATAPACK_NAME = "meko_default_datapack";
|
|
22
|
+
const NIL_CONVERSATION_ID = "00000000-0000-0000-0000-000000000000";
|
|
23
|
+
const SUGGESTION_LIMIT = 10;
|
|
16
24
|
|
|
17
25
|
export function isUuid(value) {
|
|
18
26
|
return typeof value === "string" && UUID_RE.test(value);
|
|
@@ -50,7 +58,8 @@ export function parseDatapackList(text) {
|
|
|
50
58
|
* Resolve the destination datapack for a migration.
|
|
51
59
|
*
|
|
52
60
|
* @param {object} opts
|
|
53
|
-
* @param {{callTool: Function}} opts.client
|
|
61
|
+
* @param {{callTool: Function, listTools?: Function}} opts.client
|
|
62
|
+
* Connected McpClient.
|
|
54
63
|
* @param {string|null} opts.requested Raw `--datapack` value (UUID or
|
|
55
64
|
* name) or null for default.
|
|
56
65
|
* @returns {Promise<{
|
|
@@ -68,20 +77,239 @@ export function parseDatapackList(text) {
|
|
|
68
77
|
* - `server-fallback`: requested=null and no default row found in the list;
|
|
69
78
|
* we let the server's own fallback chain handle it (id stays null).
|
|
70
79
|
*
|
|
71
|
-
* `defaultId` is the id of the `meko_default_datapack` row if visible
|
|
72
|
-
*
|
|
73
|
-
*
|
|
80
|
+
* `defaultId` is the id of the `meko_default_datapack` row if visible, so the
|
|
81
|
+
* caller can detect "user named the default explicitly" and reuse the same
|
|
82
|
+
* on-disk state file. With `datapack_search`, an explicit `requested` whose
|
|
83
|
+
* datapack has another name gets `defaultId: null` without a lookup: its id
|
|
84
|
+
* can't match the default's, and skipping the lookup keeps a failure there
|
|
85
|
+
* from failing the request.
|
|
74
86
|
*
|
|
75
87
|
* Failure cases (`ok: false`):
|
|
76
|
-
* -
|
|
77
|
-
* - response body wasn't
|
|
78
|
-
* - explicit override didn't match any
|
|
88
|
+
* - the lookup call failed (auth, network, server error).
|
|
89
|
+
* - response body wasn't recognizable.
|
|
90
|
+
* - explicit override didn't match any visible datapack.
|
|
79
91
|
*/
|
|
80
92
|
export async function resolveDestinationDatapack(opts) {
|
|
81
93
|
const { client, requested } = opts;
|
|
94
|
+
if (await hasTool(client, "datapack_search")) {
|
|
95
|
+
return resolveWithSearch(client, requested);
|
|
96
|
+
}
|
|
97
|
+
return resolveWithListScan(client, requested);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Whether the server lists `name`. A client without `listTools` (a test
|
|
102
|
+
* double) or a failed tools/list reads as "no", which selects the
|
|
103
|
+
* `datapack_list` scan every server supports.
|
|
104
|
+
*/
|
|
105
|
+
async function hasTool(client, name) {
|
|
106
|
+
if (typeof client.listTools !== "function") return false;
|
|
107
|
+
try {
|
|
108
|
+
const names = await client.listTools();
|
|
109
|
+
return Array.isArray(names) && names.includes(name);
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Parse a `datapack_search` response body into its rows, or return
|
|
117
|
+
* `{error}` naming what went wrong. Rows are summary rows (no credentials),
|
|
118
|
+
* but the body is still never echoed.
|
|
119
|
+
*
|
|
120
|
+
* @param {{ok: boolean, text?: string, error?: object}} res
|
|
121
|
+
* @returns {{rows: Array} | {error: string}}
|
|
122
|
+
*/
|
|
123
|
+
export function parseDatapackSearch(res) {
|
|
124
|
+
if (!res.ok) return { error: res.error?.message ?? "unknown" };
|
|
125
|
+
let body;
|
|
126
|
+
try {
|
|
127
|
+
body = JSON.parse(res.text ?? "");
|
|
128
|
+
} catch {
|
|
129
|
+
return { error: "unrecognized response shape" };
|
|
130
|
+
}
|
|
131
|
+
if (body && typeof body === "object" && typeof body.error === "string") {
|
|
132
|
+
const detail = typeof body.detail === "string" ? ` (${body.detail})` : "";
|
|
133
|
+
return { error: `${body.error}${detail}` };
|
|
134
|
+
}
|
|
135
|
+
if (!body || !Array.isArray(body.datapacks)) {
|
|
136
|
+
return { error: "unrecognized response shape" };
|
|
137
|
+
}
|
|
138
|
+
const rows = body.datapacks.filter(
|
|
139
|
+
(e) => e && typeof e === "object" && typeof e.datapack_id === "string",
|
|
140
|
+
);
|
|
141
|
+
return { rows };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function search(client, args) {
|
|
145
|
+
return client.callTool("datapack_search", {
|
|
146
|
+
conversation_id: NIL_CONVERSATION_ID,
|
|
147
|
+
...args,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The row named exactly `name`. An owned row is requested first, with
|
|
153
|
+
* `grant: "owner"`, so a page of newer shared copies of the same name
|
|
154
|
+
* cannot hide it. If the caller owns none, the first exact match of any
|
|
155
|
+
* grant is used.
|
|
156
|
+
*/
|
|
157
|
+
async function findByName(client, name) {
|
|
158
|
+
const owned = parseDatapackSearch(
|
|
159
|
+
await search(client, { name, grant: "owner", limit: 100 }),
|
|
160
|
+
);
|
|
161
|
+
if (owned.error) return owned;
|
|
162
|
+
const ownedHit = owned.rows.find(
|
|
163
|
+
(d) => d.datapack_name === name && d.grant === "owner",
|
|
164
|
+
);
|
|
165
|
+
if (ownedHit) return { row: ownedHit };
|
|
166
|
+
|
|
167
|
+
const found = parseDatapackSearch(await search(client, { name, limit: 100 }));
|
|
168
|
+
if (found.error) return found;
|
|
169
|
+
const exact = found.rows.filter((d) => d.datapack_name === name);
|
|
170
|
+
return { row: exact[0] ?? null };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function resolveWithSearch(client, requested) {
|
|
174
|
+
if (!requested) {
|
|
175
|
+
const def = await findByName(client, DEFAULT_DATAPACK_NAME);
|
|
176
|
+
if (def.error) {
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
message: `datapack_search failed: ${def.error}. Cannot resolve --datapack.`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const defaultRow = def.row;
|
|
183
|
+
if (defaultRow) {
|
|
184
|
+
return {
|
|
185
|
+
ok: true,
|
|
186
|
+
id: defaultRow.datapack_id,
|
|
187
|
+
name: defaultRow.datapack_name,
|
|
188
|
+
source: "default",
|
|
189
|
+
defaultId: defaultRow.datapack_id,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return { ok: true, id: null, name: null, source: "server-fallback", defaultId: null };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const found = await resolveRequested(client, requested);
|
|
196
|
+
if (!found.ok) return found;
|
|
197
|
+
const def = await defaultIdFor(client, requested, found.row);
|
|
198
|
+
if (def.error) {
|
|
199
|
+
return {
|
|
200
|
+
ok: false,
|
|
201
|
+
message:
|
|
202
|
+
`datapack_search failed: ${def.error}. Cannot tell whether --datapack ` +
|
|
203
|
+
`${requested} is your default datapack.`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
ok: true,
|
|
208
|
+
id: found.row.datapack_id,
|
|
209
|
+
name: found.row.datapack_name ?? null,
|
|
210
|
+
source: found.source,
|
|
211
|
+
defaultId: def.id,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The id of the caller's `meko_default_datapack`, or null, for comparing with
|
|
217
|
+
* an explicitly requested datapack. A row with any other name can't be the
|
|
218
|
+
* default, so the lookup runs only when the requested datapack carries the
|
|
219
|
+
* default's name or its name is unknown. That keeps a failure in a lookup the
|
|
220
|
+
* request doesn't need from failing the request.
|
|
221
|
+
*
|
|
222
|
+
* @returns {Promise<{id: string|null} | {error: string}>}
|
|
223
|
+
*/
|
|
224
|
+
async function defaultIdFor(client, requested, row) {
|
|
225
|
+
const name = row.datapack_name;
|
|
226
|
+
if (typeof name === "string" && name !== DEFAULT_DATAPACK_NAME) {
|
|
227
|
+
return { id: null };
|
|
228
|
+
}
|
|
229
|
+
// findByName already resolved the default's name, owner first.
|
|
230
|
+
if (requested === DEFAULT_DATAPACK_NAME) return { id: row.datapack_id };
|
|
231
|
+
const def = await findByName(client, DEFAULT_DATAPACK_NAME);
|
|
232
|
+
if (def.error) return def;
|
|
233
|
+
return { id: def.row?.datapack_id ?? null };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Resolve an explicit `--datapack` to `{ok: true, row, source}`, or to the
|
|
238
|
+
* `{ok: false, message}` the caller returns as is.
|
|
239
|
+
*/
|
|
240
|
+
async function resolveRequested(client, requested) {
|
|
241
|
+
if (isUuid(requested)) {
|
|
242
|
+
const res = await client.callTool("datapack_describe", {
|
|
243
|
+
datapack_id: requested,
|
|
244
|
+
conversation_id: NIL_CONVERSATION_ID,
|
|
245
|
+
});
|
|
246
|
+
const hit = parseDescribe(res);
|
|
247
|
+
if (hit.error) {
|
|
248
|
+
return {
|
|
249
|
+
ok: false,
|
|
250
|
+
message:
|
|
251
|
+
`--datapack ${requested}: no datapack with that id is visible to ` +
|
|
252
|
+
`this API key (${hit.error}).`,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
return { ok: true, row: hit.row, source: "flag-id" };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const byName = await findByName(client, requested);
|
|
259
|
+
if (byName.error) {
|
|
260
|
+
return {
|
|
261
|
+
ok: false,
|
|
262
|
+
message: `datapack_search failed: ${byName.error}. Cannot resolve --datapack.`,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
if (!byName.row) {
|
|
266
|
+
const similar = parseDatapackSearch(
|
|
267
|
+
await search(client, { query: requested, limit: SUGGESTION_LIMIT }),
|
|
268
|
+
);
|
|
269
|
+
if (similar.error) {
|
|
270
|
+
return {
|
|
271
|
+
ok: false,
|
|
272
|
+
message: `datapack_search failed: ${similar.error}. Cannot resolve --datapack.`,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const hint = similar.rows.length
|
|
276
|
+
? `Similar names: ${formatNames(similar.rows)}.`
|
|
277
|
+
: "No datapack name contains that text.";
|
|
278
|
+
return {
|
|
279
|
+
ok: false,
|
|
280
|
+
message:
|
|
281
|
+
`--datapack ${JSON.stringify(requested)}: no datapack with that name. ` +
|
|
282
|
+
hint,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
return { ok: true, row: byName.row, source: "flag-name" };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Parse a `datapack_describe` body into `{row}` or `{error}`. Only the id
|
|
290
|
+
* and name are read; the body is never echoed.
|
|
291
|
+
*/
|
|
292
|
+
function parseDescribe(res) {
|
|
293
|
+
if (!res.ok) return { error: res.error?.message ?? "unknown" };
|
|
294
|
+
let body;
|
|
295
|
+
try {
|
|
296
|
+
body = JSON.parse(res.text ?? "");
|
|
297
|
+
} catch {
|
|
298
|
+
return { error: "unrecognized response shape" };
|
|
299
|
+
}
|
|
300
|
+
if (body && typeof body === "object" && typeof body.error === "string") {
|
|
301
|
+
return { error: body.error };
|
|
302
|
+
}
|
|
303
|
+
if (!body || typeof body.datapack_id !== "string") {
|
|
304
|
+
return { error: "unrecognized response shape" };
|
|
305
|
+
}
|
|
306
|
+
return { row: body };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function resolveWithListScan(client, requested) {
|
|
82
310
|
const res = await client.callTool("datapack_list", {
|
|
83
311
|
scope: "read",
|
|
84
|
-
conversation_id:
|
|
312
|
+
conversation_id: NIL_CONVERSATION_ID,
|
|
85
313
|
});
|
|
86
314
|
if (!res.ok) {
|
|
87
315
|
return {
|
|
@@ -101,7 +329,7 @@ export async function resolveDestinationDatapack(opts) {
|
|
|
101
329
|
};
|
|
102
330
|
}
|
|
103
331
|
|
|
104
|
-
const defaultRow = list
|
|
332
|
+
const defaultRow = namedRow(list, DEFAULT_DATAPACK_NAME);
|
|
105
333
|
const defaultId = defaultRow?.datapack_id ?? null;
|
|
106
334
|
|
|
107
335
|
if (!requested) {
|
|
@@ -136,7 +364,7 @@ export async function resolveDestinationDatapack(opts) {
|
|
|
136
364
|
};
|
|
137
365
|
}
|
|
138
366
|
|
|
139
|
-
const hit = list
|
|
367
|
+
const hit = namedRow(list, requested);
|
|
140
368
|
if (!hit) {
|
|
141
369
|
return {
|
|
142
370
|
ok: false,
|
|
@@ -154,14 +382,24 @@ export async function resolveDestinationDatapack(opts) {
|
|
|
154
382
|
};
|
|
155
383
|
}
|
|
156
384
|
|
|
385
|
+
function namedRow(list, name) {
|
|
386
|
+
const exact = list.filter((d) => d.datapack_name === name);
|
|
387
|
+
return exact.find((d) => d.grant === "owner") ?? exact[0] ?? null;
|
|
388
|
+
}
|
|
389
|
+
|
|
157
390
|
/**
|
|
158
391
|
* Format a one-line "Available: a, b, c" hint. Names only — never ids,
|
|
159
392
|
* because the underlying response can carry credentials.
|
|
160
393
|
*/
|
|
161
394
|
function formatAvailable(list) {
|
|
162
|
-
const names = list
|
|
395
|
+
const names = formatNames(list);
|
|
396
|
+
if (!names) return "(no datapacks visible to this API key.)";
|
|
397
|
+
return `Available: ${names}.`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function formatNames(list) {
|
|
401
|
+
return list
|
|
163
402
|
.map((d) => d.datapack_name)
|
|
164
|
-
.filter((n) => typeof n === "string" && n.length > 0)
|
|
165
|
-
|
|
166
|
-
return `Available: ${names.join(", ")}.`;
|
|
403
|
+
.filter((n) => typeof n === "string" && n.length > 0)
|
|
404
|
+
.join(", ");
|
|
167
405
|
}
|
|
@@ -79,6 +79,40 @@ export class McpClient {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Names of the tools the server lists over the open session. Throws when
|
|
84
|
+
* the request fails, so callers can decide how to degrade.
|
|
85
|
+
*
|
|
86
|
+
* @param {{timeoutMs?: number}} [opts]
|
|
87
|
+
* @returns {Promise<string[]>}
|
|
88
|
+
*/
|
|
89
|
+
async listTools(opts = {}) {
|
|
90
|
+
await this._acquire();
|
|
91
|
+
try {
|
|
92
|
+
const res = await jsonRpcRequest(
|
|
93
|
+
this.url,
|
|
94
|
+
{ jsonrpc: "2.0", id: this._reqId++, method: "tools/list", params: {} },
|
|
95
|
+
this.apiKey,
|
|
96
|
+
this.sessionId,
|
|
97
|
+
{ timeoutMs: opts.timeoutMs ?? this.timeoutMs },
|
|
98
|
+
);
|
|
99
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
100
|
+
const e = new Error(`tools/list returned HTTP ${res.statusCode}`);
|
|
101
|
+
e.statusCode = res.statusCode;
|
|
102
|
+
throw e;
|
|
103
|
+
}
|
|
104
|
+
const parsed = JSON.parse(res.body);
|
|
105
|
+
if (parsed.error) {
|
|
106
|
+
throw new Error(`tools/list error: ${parsed.error.message ?? "unknown"}`);
|
|
107
|
+
}
|
|
108
|
+
const tools = parsed.result?.tools;
|
|
109
|
+
if (!Array.isArray(tools)) throw new Error("tools/list returned no tools array");
|
|
110
|
+
return tools.map((t) => t?.name).filter((n) => typeof n === "string");
|
|
111
|
+
} finally {
|
|
112
|
+
this._release();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
82
116
|
async _callWithRetry(name, args, opts) {
|
|
83
117
|
const maxRetries = opts.maxRetries ?? 3;
|
|
84
118
|
let lastErr = null;
|
|
@@ -287,7 +287,7 @@ export async function runMigration(opts) {
|
|
|
287
287
|
return { ok: false, summary: { error: "mcp_connect_failed", detail: err.message }, counts: prog.counts };
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
-
// Resolve --datapack against the live
|
|
290
|
+
// Resolve --datapack against the live server so every subsequent
|
|
291
291
|
// tools/call carries an explicit datapack_id. Without this we'd be silently
|
|
292
292
|
// relying on the server's tenant-context default — fine when it's correct,
|
|
293
293
|
// invisible when it isn't.
|
package/lib/migrate.mjs
CHANGED
|
@@ -20,7 +20,7 @@ import * as progress from "./migrate/progress.mjs";
|
|
|
20
20
|
import { datapackKeyFromUrl } from "./migrate/id.mjs";
|
|
21
21
|
import { resolveDestinationDatapack } from "./migrate/datapack.mjs";
|
|
22
22
|
import { McpClient } from "./migrate/mcp-client.mjs";
|
|
23
|
-
import { selfInvocation } from "./clients/common.mjs";
|
|
23
|
+
import { isLocalUrl, selfInvocation } from "./clients/common.mjs";
|
|
24
24
|
import { askYesNo } from "./prompt.mjs";
|
|
25
25
|
|
|
26
26
|
function home() {
|
|
@@ -302,7 +302,7 @@ export async function startMigration(opts) {
|
|
|
302
302
|
// Pre-flight: resolve the destination datapack *before* taking the lock or
|
|
303
303
|
// forking the child. A typo in --datapack should fail fast with a useful
|
|
304
304
|
// error, not after the child has spun up. We open a short-lived MCP client
|
|
305
|
-
// here purely for the
|
|
305
|
+
// here purely for the datapack lookup; the child opens its own session.
|
|
306
306
|
// The resolved id isn't persisted into passthrough — the child re-resolves,
|
|
307
307
|
// which costs one extra cheap read but keeps the wire-level routing
|
|
308
308
|
// logic in a single place.
|
|
@@ -764,7 +764,3 @@ export async function stopMigration() {
|
|
|
764
764
|
message: `Force-killed pid=${holder.pid} (run-id=${holder.runId}).`,
|
|
765
765
|
};
|
|
766
766
|
}
|
|
767
|
-
|
|
768
|
-
function isLocalUrl(url) {
|
|
769
|
-
return /localhost|127\.0\.0\.1/.test(url ?? "");
|
|
770
|
-
}
|
package/lib/validate.mjs
CHANGED
|
@@ -38,8 +38,13 @@ const CORE_TOOLS = [
|
|
|
38
38
|
/** Nil UUID used as the conversation_id placeholder for the canary call. */
|
|
39
39
|
const NIL_UUID = "00000000-0000-0000-0000-000000000000";
|
|
40
40
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Tool the canary invokes. `datapack_search(limit=1)` reads one page whatever
|
|
43
|
+
* the account size; a server that predates it gets `datapack_list`, which
|
|
44
|
+
* reads the whole account.
|
|
45
|
+
*/
|
|
46
|
+
const CANARY_TOOL = "datapack_search";
|
|
47
|
+
const LEGACY_CANARY_TOOL = "datapack_list";
|
|
43
48
|
|
|
44
49
|
// ---------------------------------------------------------------------------
|
|
45
50
|
// 1. Config validation
|
|
@@ -139,9 +144,10 @@ export async function validateTools(url, apiKey) {
|
|
|
139
144
|
* The canary's job is to prove the part of the stack that connectivity +
|
|
140
145
|
* tools/list don't cover: auth was accepted as a real Meko identity, the
|
|
141
146
|
* tenant resolver mapped it to an account, and the datapack routing wired
|
|
142
|
-
* up the user's actual workspace. We invoke `
|
|
143
|
-
*
|
|
144
|
-
*
|
|
147
|
+
* up the user's actual workspace. We invoke `datapack_search(limit=1)` for
|
|
148
|
+
* this, or `datapack_list` on a server whose tools/list has no
|
|
149
|
+
* `datapack_search`. Either is a single read-only call against the system
|
|
150
|
+
* schema and returns deterministically (every authenticated user has at
|
|
145
151
|
* least one datapack: their default).
|
|
146
152
|
*
|
|
147
153
|
* Why not memory_add? The previous canary did a write/read/delete round-trip
|
|
@@ -166,11 +172,13 @@ export async function validateTools(url, apiKey) {
|
|
|
166
172
|
* initialize handshake is performed.
|
|
167
173
|
* @param {number} [options.timeoutMs] - Override the canary call's transport timeout.
|
|
168
174
|
* Defaults to DEFAULT_REQUEST_TIMEOUT_MS.
|
|
175
|
+
* @param {string[]} [options.toolNames] - Tool names from an earlier tools/list.
|
|
176
|
+
* If omitted, the canary lists tools itself.
|
|
169
177
|
* @returns {Promise<{
|
|
170
178
|
* ok: boolean,
|
|
171
179
|
* message: string,
|
|
172
180
|
* datapack_count: number|null,
|
|
173
|
-
* step?: "initialize"|"datapack_list",
|
|
181
|
+
* step?: "initialize"|"datapack_search"|"datapack_list",
|
|
174
182
|
* }>}
|
|
175
183
|
*/
|
|
176
184
|
export async function validateCanary(url, apiKey, options = {}) {
|
|
@@ -190,11 +198,77 @@ export async function validateCanary(url, apiKey, options = {}) {
|
|
|
190
198
|
sessionId = init.sessionId;
|
|
191
199
|
}
|
|
192
200
|
|
|
201
|
+
let toolNames = options.toolNames;
|
|
202
|
+
if (!Array.isArray(toolNames)) {
|
|
203
|
+
// A failed listing just means we cannot tell; datapack_list works on
|
|
204
|
+
// every server.
|
|
205
|
+
const listed = await sendToolsList(url, apiKey, sessionId);
|
|
206
|
+
toolNames = listed.tools;
|
|
207
|
+
}
|
|
208
|
+
if (toolNames.includes(CANARY_TOOL)) {
|
|
209
|
+
return searchCanary(url, apiKey, sessionId, timeoutMs);
|
|
210
|
+
}
|
|
211
|
+
return listCanary(url, apiKey, sessionId, timeoutMs);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function searchCanary(url, apiKey, sessionId, timeoutMs) {
|
|
193
215
|
const callRes = await mcpCallTool(
|
|
194
216
|
url,
|
|
195
217
|
apiKey,
|
|
196
218
|
sessionId,
|
|
197
219
|
CANARY_TOOL,
|
|
220
|
+
{ conversation_id: NIL_UUID, limit: 1 },
|
|
221
|
+
{ timeoutMs },
|
|
222
|
+
);
|
|
223
|
+
if (!callRes.ok) {
|
|
224
|
+
return {
|
|
225
|
+
ok: false,
|
|
226
|
+
datapack_count: null,
|
|
227
|
+
step: CANARY_TOOL,
|
|
228
|
+
message: `Canary: ${callRes.message}`,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const datapacks = parseDatapackSearchPage(callRes.text);
|
|
233
|
+
if (!datapacks) {
|
|
234
|
+
return {
|
|
235
|
+
ok: false,
|
|
236
|
+
datapack_count: null,
|
|
237
|
+
step: CANARY_TOOL,
|
|
238
|
+
message:
|
|
239
|
+
`Canary: ${CANARY_TOOL} returned an unexpected response shape ` +
|
|
240
|
+
`(expected an object with a datapacks array). Check the server logs.`,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (datapacks.length === 0) {
|
|
244
|
+
return zeroDatapacks(CANARY_TOOL);
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
ok: true,
|
|
248
|
+
datapack_count: datapacks.length,
|
|
249
|
+
message: `Canary OK — ${CANARY_TOOL} found a datapack.`,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function zeroDatapacks(tool) {
|
|
254
|
+
return {
|
|
255
|
+
ok: false,
|
|
256
|
+
datapack_count: 0,
|
|
257
|
+
step: tool,
|
|
258
|
+
message:
|
|
259
|
+
`Canary: ${tool} returned zero datapacks. Every authenticated ` +
|
|
260
|
+
`Meko user should have at least their default datapack — this likely ` +
|
|
261
|
+
`means the API token authenticated but resolved to the wrong tenant. ` +
|
|
262
|
+
`Verify the API key matches the tenant the URL points at.`,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function listCanary(url, apiKey, sessionId, timeoutMs) {
|
|
267
|
+
const callRes = await mcpCallTool(
|
|
268
|
+
url,
|
|
269
|
+
apiKey,
|
|
270
|
+
sessionId,
|
|
271
|
+
LEGACY_CANARY_TOOL,
|
|
198
272
|
{
|
|
199
273
|
scope: "read",
|
|
200
274
|
conversation_id: NIL_UUID,
|
|
@@ -210,7 +284,7 @@ export async function validateCanary(url, apiKey, options = {}) {
|
|
|
210
284
|
return {
|
|
211
285
|
ok: false,
|
|
212
286
|
datapack_count: null,
|
|
213
|
-
step:
|
|
287
|
+
step: LEGACY_CANARY_TOOL,
|
|
214
288
|
message: `Canary: ${callRes.message}`,
|
|
215
289
|
};
|
|
216
290
|
}
|
|
@@ -224,30 +298,21 @@ export async function validateCanary(url, apiKey, options = {}) {
|
|
|
224
298
|
return {
|
|
225
299
|
ok: false,
|
|
226
300
|
datapack_count: null,
|
|
227
|
-
step:
|
|
301
|
+
step: LEGACY_CANARY_TOOL,
|
|
228
302
|
message:
|
|
229
|
-
`Canary: ${
|
|
303
|
+
`Canary: ${LEGACY_CANARY_TOOL} returned an unexpected response shape ` +
|
|
230
304
|
`(expected a JSON array of datapack objects). Check the server logs.`,
|
|
231
305
|
};
|
|
232
306
|
}
|
|
233
307
|
|
|
234
308
|
if (datapacks.length === 0) {
|
|
235
|
-
return
|
|
236
|
-
ok: false,
|
|
237
|
-
datapack_count: 0,
|
|
238
|
-
step: CANARY_TOOL,
|
|
239
|
-
message:
|
|
240
|
-
`Canary: ${CANARY_TOOL} returned zero datapacks. Every authenticated ` +
|
|
241
|
-
`Meko user should have at least their default datapack — this likely ` +
|
|
242
|
-
`means the API token authenticated but resolved to the wrong tenant. ` +
|
|
243
|
-
`Verify the API key matches the tenant the URL points at.`,
|
|
244
|
-
};
|
|
309
|
+
return zeroDatapacks(LEGACY_CANARY_TOOL);
|
|
245
310
|
}
|
|
246
311
|
|
|
247
312
|
return {
|
|
248
313
|
ok: true,
|
|
249
314
|
datapack_count: datapacks.length,
|
|
250
|
-
message: `Canary OK — ${
|
|
315
|
+
message: `Canary OK — ${LEGACY_CANARY_TOOL} returned ${datapacks.length} datapack(s).`,
|
|
251
316
|
};
|
|
252
317
|
}
|
|
253
318
|
|
|
@@ -263,8 +328,8 @@ export async function validateCanary(url, apiKey, options = {}) {
|
|
|
263
328
|
* canary reuses that same session too.
|
|
264
329
|
*
|
|
265
330
|
* The canary is skipped (not failed) when tools/list didn't pass — the
|
|
266
|
-
* canary depends on `datapack_list`
|
|
267
|
-
* already surfaces that.
|
|
331
|
+
* canary depends on `datapack_list` (a core tool) or `datapack_search`
|
|
332
|
+
* being present, and a tools failure already surfaces that.
|
|
268
333
|
*
|
|
269
334
|
* @param {string} url - Full MCP endpoint URL.
|
|
270
335
|
* @param {string|null} apiKey - Optional API key.
|
|
@@ -292,6 +357,7 @@ export async function runValidation(url, apiKey, serverName = "meko", options =
|
|
|
292
357
|
if (tools.ok && connectivity.ok) {
|
|
293
358
|
canary = await validateCanary(url, apiKey, {
|
|
294
359
|
sessionId: connectivity.sessionId,
|
|
360
|
+
toolNames: tools.tools,
|
|
295
361
|
});
|
|
296
362
|
} else {
|
|
297
363
|
canary = {
|
|
@@ -508,6 +574,29 @@ function extractFirstText(result) {
|
|
|
508
574
|
return null;
|
|
509
575
|
}
|
|
510
576
|
|
|
577
|
+
/**
|
|
578
|
+
* Parse a `datapack_search` response body and return its `datapacks` rows,
|
|
579
|
+
* or `null` if the body is not a recognizable page.
|
|
580
|
+
*
|
|
581
|
+
* @param {string|null} text
|
|
582
|
+
* @returns {Array|null}
|
|
583
|
+
*/
|
|
584
|
+
function parseDatapackSearchPage(text) {
|
|
585
|
+
if (!text) return null;
|
|
586
|
+
let body;
|
|
587
|
+
try {
|
|
588
|
+
body = JSON.parse(text);
|
|
589
|
+
} catch {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
if (!body || typeof body !== "object" || !Array.isArray(body.datapacks)) return null;
|
|
593
|
+
const rows = body.datapacks;
|
|
594
|
+
const allLookLikeDatapacks = rows.every(
|
|
595
|
+
(entry) => entry && typeof entry === "object" && typeof entry.datapack_id === "string",
|
|
596
|
+
);
|
|
597
|
+
return allLookLikeDatapacks ? rows : null;
|
|
598
|
+
}
|
|
599
|
+
|
|
511
600
|
/**
|
|
512
601
|
* Parse a `datapack_list` response body and return the array of datapacks,
|
|
513
602
|
* or `null` if the body is not a recognizable datapack list.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yugabytedb/meko-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.1",
|
|
4
4
|
"description": "Installer for Meko across Claude, Cursor, Codex, and Kiro clients",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
"prepack": "node scripts/bundle-assets.mjs",
|
|
19
19
|
"postpack": "node scripts/bundle-assets.mjs --clean",
|
|
20
20
|
"bundle-assets": "node scripts/bundle-assets.mjs",
|
|
21
|
-
"test": "node --test \"test/*.mjs\" && node --test \"test/serial/*.mjs\"",
|
|
21
|
+
"test": "node --import ./test/helpers/hermetic-env.mjs --test \"test/*.mjs\" && node --import ./test/helpers/hermetic-env.mjs --test \"test/serial/*.mjs\"",
|
|
22
22
|
"smoke": "./scripts/prepublish-smoke.sh",
|
|
23
|
-
"test:unit": "node --test test/config.test.mjs test/validate.test.mjs test/clients.test.mjs",
|
|
24
|
-
"test:integration": "RUN_INSTALLER_INTEGRATION=1 node --test test/installer.test.mjs",
|
|
25
|
-
"test:integration:cloud": "RUN_INSTALLER_INTEGRATION=1 node --test test/installer.test.mjs"
|
|
23
|
+
"test:unit": "node --import ./test/helpers/hermetic-env.mjs --test test/config.test.mjs test/validate.test.mjs test/clients.test.mjs",
|
|
24
|
+
"test:integration": "RUN_INSTALLER_INTEGRATION=1 node --import ./test/helpers/hermetic-env.mjs --test test/installer.test.mjs",
|
|
25
|
+
"test:integration:cloud": "RUN_INSTALLER_INTEGRATION=1 node --import ./test/helpers/hermetic-env.mjs --test test/installer.test.mjs"
|
|
26
26
|
},
|
|
27
27
|
"engines": {
|
|
28
28
|
"node": ">=20.18.1"
|