@yugabytedb/meko-mcp 1.2.0 → 2.0.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.
Files changed (44) hide show
  1. package/README.md +28 -19
  2. package/assets/hooks-handlers/before-submit-prompt.sh +2 -2
  3. package/assets/hooks-handlers/lib/capture.js +209 -144
  4. package/assets/hooks-handlers/lib/checkpoint-timer.js +23 -11
  5. package/assets/hooks-handlers/session-start.sh +6 -2
  6. package/assets/scripts/meko-statusline.sh +119 -0
  7. package/assets/scripts/package-desktop-skill.sh +27 -24
  8. package/assets/skills/meko-mcp-tools/SKILL.md +42 -50
  9. package/assets/skills/meko-mcp-tools/references/_sections.md +1 -1
  10. package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +75 -150
  11. package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +30 -36
  12. package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +6 -22
  13. package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +9 -8
  14. package/assets/skills/meko-mcp-tools/references/tools-overview.md +35 -42
  15. package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +28 -92
  16. package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +5 -9
  17. package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +1 -81
  18. package/assets/skills/meko-mcp-tools-desktop/SKILL.md +100 -107
  19. package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +41 -11
  20. package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +1 -1
  21. package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +71 -150
  22. package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +12 -18
  23. package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +12 -15
  24. package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +35 -42
  25. package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +28 -92
  26. package/assets/skills/meko-mcp-tools-desktop/references/tools-scope-permissions.md +5 -9
  27. package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +1 -81
  28. package/assets/skills/meko-select-datapack-desktop/SKILL.md +198 -0
  29. package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +121 -0
  30. package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +66 -0
  31. package/bin/create-meko-setup.mjs +29 -0
  32. package/lib/clients/claude-code.mjs +133 -6
  33. package/lib/clients/claude-desktop.mjs +37 -12
  34. package/lib/clients/codex.mjs +306 -5
  35. package/lib/clients/common.mjs +41 -1
  36. package/lib/clients/cursor.mjs +75 -19
  37. package/lib/config.mjs +26 -0
  38. package/lib/installer.mjs +24 -0
  39. package/lib/mcp-transport.mjs +5 -0
  40. package/lib/validate.mjs +0 -1
  41. package/package.json +1 -1
  42. package/scripts/bundle-assets.mjs +8 -0
  43. package/assets/skills/meko-mcp-tools/references/tools-db-query-patterns.md +0 -38
  44. package/assets/skills/meko-mcp-tools/references/tools-write-guardrails.md +0 -30
@@ -0,0 +1,66 @@
1
+ # Troubleshooting — meko-select-datapack-desktop
2
+
3
+ ## Symptom: I pinned a datapack but the next Meko call doesn't use it
4
+
5
+ **Likely causes**
6
+
7
+ 1. **You forgot the per-turn `memory_search`.** Desktop has no SessionStart hook, so the skill teaches you to read the pin yourself at the top of every Meko-touching turn. If you skip that lookup, you'll send Meko calls with no `datapack_id` and the server resolves the default. Add the lookup; cache the result for the rest of the turn.
8
+ 2. **You used `agent_id="meko_agent"` for the pin write or read.** The pin lives under `agent_id="claude_desktop"`, exclusively. Cross-project facts go under `meko_agent`, but the pin is a Desktop-specific preference and should stay scoped.
9
+ 3. **The pin write failed silently.** Always verify-after-write — call `memory_search` for `meko_active_datapack` immediately after `memory_add` and confirm the row appears. If it doesn't, the write was lost (latency, server error, wrong scope) and the pin doesn't actually exist.
10
+
11
+ ## Symptom: `memory_search` returns multiple pin memories
12
+
13
+ You (or an earlier run) created duplicates by using `memory_add` instead of `memory_update` on a switch. Resolution:
14
+
15
+ 1. Pick the most recent (highest `selected_at` in the text body).
16
+ 2. `memory_delete_by_id` the others.
17
+ 3. From now on, switch via `memory_update` on the surviving id — never `memory_add`.
18
+
19
+ The cookbook's Flow 2 demonstrates the correct switch path.
20
+
21
+ ## Symptom: A pinned datapack returns "not found" when used
22
+
23
+ The pin became stale — the datapack was deleted in another client. Tell the user:
24
+
25
+ ```
26
+ The pinned datapack `<name>` (`<id>`) no longer exists on the server.
27
+ Run `meko-select-datapack-desktop` to pick a fresh one, or `clear` to unpin.
28
+ ```
29
+
30
+ Then either re-run the skill or delete the pin memory. Do not silently fall back to the default — the user explicitly asked for a specific datapack.
31
+
32
+ ## Symptom: Substring search matches multiple datapacks
33
+
34
+ Never auto-pick when there's ambiguity. Re-render just the matching rows numbered from 1, and ask the user to pick a number from the filtered subset. Same as the Claude Code variant.
35
+
36
+ ## Symptom: `datapack_list` returns an empty array
37
+
38
+ The user has zero datapacks. Print:
39
+
40
+ ```
41
+ You don't have any datapacks yet. Create one with:
42
+
43
+ datapack_create(scope="admin", name="<your-datapack-name>")
44
+
45
+ Or visit the Meko Cloud console (Datapacks → New datapack).
46
+ ```
47
+
48
+ Stop. Don't `memory_add` an empty pin.
49
+
50
+ ## Symptom: User typed `cancel` after the table rendered
51
+
52
+ Leave the existing pin memory alone. Print:
53
+
54
+ ```
55
+ Cancelled. Active datapack unchanged.
56
+ ```
57
+
58
+ Do NOT delete on `cancel` — that's `clear`'s job.
59
+
60
+ ## Symptom: `memory_search` for the pin times out
61
+
62
+ Memory tools are slow on prod (search 2-6s observed). If the search exceeds a reasonable budget, fall back to the server default for that single call and tell the user *"Couldn't read the active-datapack pin within budget — using server default for this Meko call."* Don't guess a UUID; don't silently retry indefinitely.
63
+
64
+ ## Symptom: The pin survived a `claude_desktop` profile reset / app reinstall
65
+
66
+ That's expected — the pin is a server-side memory, not local state. To explicitly forget across reinstalls, run the skill with `clear` BEFORE wiping local state, or `memory_delete_by_id` the pin row directly.
@@ -28,6 +28,8 @@ const options = {
28
28
  'cursor-hook-mode': { type: 'string', default: 'native' },
29
29
  'skip-skills': { type: 'boolean', default: false },
30
30
  'skip-validation': { type: 'boolean', default: false },
31
+ statusline: { type: 'boolean', default: false },
32
+ 'no-statusline': { type: 'boolean', default: false },
31
33
  'no-canary': { type: 'boolean', default: false },
32
34
  'dry-run': { type: 'boolean', default: false },
33
35
  'check-updates': { type: 'boolean', default: false },
@@ -111,6 +113,11 @@ Options:
111
113
  --name <name> MCP server name in config (default: meko)
112
114
  --cursor-hook-mode Cursor hook mode: native or claude-compat (default: native)
113
115
  --skip-skills Skip plugin and skills installation
116
+ --statusline Wire the Meko datapack-pin indicator into Claude Code's
117
+ statusline (composes with any existing statusLine).
118
+ Default: ON under --yes, prompted otherwise.
119
+ Claude Code only.
120
+ --no-statusline Skip statusline wiring even under --yes.
114
121
  --skip-validation Skip post-install validation
115
122
  --no-canary Skip the canary datapack_list call. The
116
123
  connectivity + tools/list checks still run.
@@ -398,6 +405,11 @@ async function main() {
398
405
  let url = flags.url ?? null;
399
406
  let apiKey = normalizeApiKey(flags['api-key']);
400
407
  let skipSkills = flags['skip-skills'];
408
+ // Statusline: --no-statusline always wins. --statusline forces on. Otherwise
409
+ // default to on under --yes (programmatic), and prompt with Y default
410
+ // interactively. Resolved later after the interactive prompts since the
411
+ // flag may flip in the interactive flow.
412
+ let installStatusline = !flags['no-statusline'] && (flags.statusline || flags.yes);
401
413
  // Tracks whether the full interactive flow already prompted for the API
402
414
  // key. The Cloud-URL fallback below should only run for the
403
415
  // semi-interactive `--url ... --client ...` paste path — otherwise
@@ -498,6 +510,19 @@ async function main() {
498
510
  if (!installSkills) {
499
511
  skipSkills = true;
500
512
  }
513
+
514
+ // Step 5 — statusline (Claude Code only). --no-statusline / --statusline
515
+ // skip the prompt; otherwise ask with Y default. The prompt only matters
516
+ // when at least one selected client is claude-code; for desktop / cursor /
517
+ // codex the install() orchestrator simply skips the step (the adapters
518
+ // don't expose installStatusline()).
519
+ if (!flags['no-statusline'] && !flags.statusline && clients.includes('claude-code')) {
520
+ installStatusline = await askYesNo(
521
+ 'Show the active Meko datapack in Claude Code\'s statusline? ' +
522
+ '(composes with any existing statusLine — non-destructive)',
523
+ true,
524
+ );
525
+ }
501
526
  }
502
527
 
503
528
  // Validation — URL must be set by now
@@ -544,6 +569,10 @@ async function main() {
544
569
  skipSkills,
545
570
  skipValidation: flags['skip-validation'],
546
571
  skipCanary: flags['no-canary'],
572
+ // Statusline is Claude Code only. For other clients the orchestrator
573
+ // skips the step regardless of the flag because the adapter doesn't
574
+ // expose installStatusline(); explicit guard keeps step counts honest.
575
+ installStatusline: client === 'claude-code' && installStatusline,
547
576
  dryRun: flags['dry-run'],
548
577
  verbose: flags.verbose,
549
578
  yes: flags.yes,
@@ -1,4 +1,4 @@
1
- import { mkdirSync } from "node:fs";
1
+ import { chmodSync, copyFileSync, mkdirSync, unlinkSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
 
@@ -14,8 +14,10 @@ import {
14
14
  } from "../config.mjs";
15
15
  import { detectClaude, detectExistingMeko, getClaudeCodeConfigPath } from "../detect.mjs";
16
16
  import {
17
+ MEKO_MCP_USER_AGENT,
17
18
  redactKey,
18
19
  resolveHooksRoot,
20
+ resolveStatuslineSource,
19
21
  run,
20
22
  shellQuote,
21
23
  writeInstallerMarker,
@@ -44,15 +46,23 @@ const MEKO_MARKETPLACE_NAME = "meko-agent-skills";
44
46
 
45
47
  // Meko MCP tools to union into `permissions.allow` so Claude Code resolves
46
48
  // them without the deferred-tools ToolSearch round-trip (issue #5). The set
47
- // is intentionally tight — only tools the agent calls proactively per
48
- // the meko-mcp-tools skill — to avoid bloating every user's settings.
49
+ // is intentionally tight — the memory/conversation tools the agent actually
50
+ // reaches for per the meko-mcp-tools skill — to avoid bloating every user's
51
+ // settings.
52
+ //
53
+ // `memory_add` stays on this list even though the skill no longer tells the
54
+ // agent to fire it proactively (server-side extraction now captures
55
+ // conversationally-stated facts automatically). It's kept because the skill
56
+ // still uses memory_add for the narrow explicit-save cases — "remember this",
57
+ // output-only/tool-derived facts, and overwriting corrections — and pre-
58
+ // allowing it keeps those calls prompt-free. `conversation_add_message` is the
59
+ // tool that now drives capture-plus-extraction, so it must resolve instantly.
49
60
  //
50
61
  // NOTE: `permissions.allow` is verified to cover user-prompt bypass. Whether
51
62
  // it also bypasses the deferred-tools schema-fetch gate must be re-verified
52
63
  // empirically before this flag is considered load-bearing for #5. Tracked
53
64
  // as Phase 10.1 in skills/TEST_PLAN.md.
54
65
  const MEKO_ALWAYS_AVAILABLE_TOOLS = [
55
- "mcp__meko__db_summarize_database",
56
66
  "mcp__meko__memory_add",
57
67
  "mcp__meko__memory_search",
58
68
  "mcp__meko__memory_get_all",
@@ -125,7 +135,8 @@ export function createClaudeCodeAdapter(options = {}) {
125
135
  buildRegisterCommand({ name, url, scope, apiKey }) {
126
136
  let cmd =
127
137
  `claude mcp add --transport http --scope ${shellQuote(scope)} ` +
128
- `${shellQuote(name)} ${shellQuote(url)}`;
138
+ `${shellQuote(name)} ${shellQuote(url)} ` +
139
+ `--header ${shellQuote(`User-Agent: ${MEKO_MCP_USER_AGENT}`)}`;
129
140
  if (apiKey) {
130
141
  cmd += ` --header ${shellQuote(`Authorization: Bearer ${apiKey}`)}`;
131
142
  }
@@ -207,14 +218,85 @@ export function createClaudeCodeAdapter(options = {}) {
207
218
  );
208
219
  }
209
220
  },
221
+ /**
222
+ * Wire the Meko datapack-pin statusline contribution into ~/.claude/settings.json.
223
+ *
224
+ * Composable: if the user already has a `statusLine.command`, that command
225
+ * is preserved as the inner command and our wrapper appends `Meko: <name>`
226
+ * after its output. Idempotent: if our wrapper is already wired, no-op.
227
+ *
228
+ * Restore-on-uninstall: the original command is preserved in
229
+ * settings.json under `_meko.statusLine.previousCommand` so `uninstall()`
230
+ * can restore it. Absent that key (fresh install), uninstall removes the
231
+ * whole `statusLine` block.
232
+ */
233
+ installStatusline({ dryRun, verbose, log, settingsPath }) {
234
+ const scriptSource = resolveStatuslineSource();
235
+ const scriptDest = join(homedir(), ".claude", "meko-statusline.sh");
236
+
237
+ const existing = readJsonFile(settingsPath) ?? {};
238
+ const existingCmd =
239
+ existing?.statusLine?.type === "command" && typeof existing?.statusLine?.command === "string"
240
+ ? existing.statusLine.command.trim()
241
+ : "";
242
+
243
+ // Detect prior install of our wrapper. Re-wiring is a no-op for the
244
+ // command (already points at the wrapper); we still re-copy the script
245
+ // so a refreshed installer ships an updated copy.
246
+ const alreadyWired = existingCmd.includes("meko-statusline.sh");
247
+ const innerCmd =
248
+ alreadyWired
249
+ ? (existing?._meko?.statusLine?.previousCommand ?? "")
250
+ : existingCmd;
251
+
252
+ const wrapperCmd = innerCmd
253
+ ? `${shellQuote(scriptDest)} --inner-cmd ${shellQuote(innerCmd)}`
254
+ : shellQuote(scriptDest);
255
+
256
+ if (dryRun) {
257
+ log.dim(` Would copy ${scriptSource} → ${scriptDest}`);
258
+ log.dim(` Would set ${settingsPath} statusLine.command = ${wrapperCmd}`);
259
+ if (innerCmd) log.dim(` (preserving prior inner command: ${innerCmd})`);
260
+ return;
261
+ }
262
+
263
+ mkdirSync(join(homedir(), ".claude"), { recursive: true });
264
+ copyFileSync(scriptSource, scriptDest);
265
+ chmodSync(scriptDest, 0o755);
266
+
267
+ const patch = {
268
+ statusLine: { type: "command", command: wrapperCmd },
269
+ };
270
+ // Stash the inner command so uninstall can restore it. Only write this
271
+ // namespace on the first install (when alreadyWired is false), so a
272
+ // re-install never overwrites the genuine original with our wrapper.
273
+ if (!alreadyWired) {
274
+ patch._meko = { statusLine: { previousCommand: innerCmd || null } };
275
+ }
276
+ mergeSettings(settingsPath, patch);
277
+
278
+ if (verbose) {
279
+ log.dim(` Statusline script: ${scriptDest}`);
280
+ log.dim(` Wrapper command: ${wrapperCmd}`);
281
+ if (innerCmd) log.dim(` Inner command preserved: ${innerCmd}`);
282
+ if (alreadyWired) log.dim(` (refreshed existing wrapper, no settings change beyond script copy)`);
283
+ }
284
+ },
210
285
  installSkills({ scope, dryRun, verbose, log }) {
211
286
  // NB: we deliberately do NOT pass --sparse. Claude CLI's sparse
212
287
  // path filter silently produces an empty clone for our layout,
213
288
  // leaving the marketplace.json missing. Full clone is small
214
289
  // (~few MB, mostly markdown) and is the only way that reliably
215
290
  // succeeds today.
291
+ // Marketplace source is normally the upstream GitHub repo. A dev/e2e
292
+ // override lets a test install the plugin from a LOCAL working-tree
293
+ // checkout (a directory path) so unreleased skill changes can be
294
+ // exercised end-to-end before they land on `yugabyte/meko-mcp-server`.
295
+ // Default is unchanged; only set MEKO_PLUGIN_MARKETPLACE_SOURCE in tests.
296
+ const marketplaceSource =
297
+ process.env.MEKO_PLUGIN_MARKETPLACE_SOURCE || "yugabyte/meko-mcp-server";
216
298
  const marketplaceCmd =
217
- `claude plugin marketplace add yugabyte/meko-mcp-server --scope ${scope}`;
299
+ `claude plugin marketplace add ${shellQuote(marketplaceSource)} --scope ${shellQuote(scope)}`;
218
300
  // `claude plugin list` dropped --scope in CLI 2.1.x; it prints all
219
301
  // scopes. The match-by-token regex used below is scope-agnostic by
220
302
  // design — `plugin update` failing with "not found" on a
@@ -393,6 +475,44 @@ export function createClaudeCodeAdapter(options = {}) {
393
475
  results.push({ key: "hooks", ok: false, message: `Could not prune Meko hook entries: ${err.message}` });
394
476
  }
395
477
 
478
+ try {
479
+ // Restore the user's prior statusLine.command (if any) and remove our
480
+ // bookkeeping. If no prior command was preserved, remove the whole
481
+ // statusLine block — installStatusline() only added it on first
482
+ // install, so we own it cleanly.
483
+ const cfg = readJsonFile(settingsPath) ?? {};
484
+ const wrapperWired =
485
+ cfg?.statusLine?.type === "command" &&
486
+ typeof cfg?.statusLine?.command === "string" &&
487
+ cfg.statusLine.command.includes("meko-statusline.sh");
488
+ if (wrapperWired) {
489
+ const prev = cfg?._meko?.statusLine?.previousCommand;
490
+ if (typeof prev === "string" && prev.length > 0) {
491
+ cfg.statusLine = { type: "command", command: prev };
492
+ results.push({ key: "statusline", ok: true, message: "Restored prior statusLine.command." });
493
+ } else {
494
+ delete cfg.statusLine;
495
+ results.push({ key: "statusline", ok: true, message: "Removed Meko statusLine wrapper." });
496
+ }
497
+ if (cfg?._meko?.statusLine) {
498
+ delete cfg._meko.statusLine;
499
+ if (cfg._meko && Object.keys(cfg._meko).length === 0) delete cfg._meko;
500
+ }
501
+ writeJsonFile(settingsPath, cfg);
502
+ // Remove the copied script too — installStatusline() owns it, so
503
+ // leaving it behind would be a stale orphan after uninstall.
504
+ try {
505
+ unlinkSync(join(homedir(), ".claude", "meko-statusline.sh"));
506
+ } catch {
507
+ // Best-effort: absent file (ENOENT) or unreadable dir is fine.
508
+ }
509
+ } else {
510
+ results.push({ key: "statusline", ok: false, message: "Statusline wrapper was not installed (nothing to remove)." });
511
+ }
512
+ } catch (err) {
513
+ results.push({ key: "statusline", ok: false, message: `Could not restore statusLine: ${err.message}` });
514
+ }
515
+
396
516
  try {
397
517
  // Qualified plugin@marketplace form — CLI 2.1.x rejects the bare
398
518
  // name (issue #63). Before: `catch` swallowed every failure as
@@ -460,6 +580,13 @@ export function createClaudeCodeAdapter(options = {}) {
460
580
  }
461
581
 
462
582
  const authHeader = getHeader(entry.headers, "Authorization");
583
+ const userAgent = getHeader(entry.headers, "User-Agent");
584
+ if (!userAgent) {
585
+ return {
586
+ ok: false,
587
+ message: `Server "${serverName}" in ${configPath} is missing the User-Agent header required by the Cloud WAF.`,
588
+ };
589
+ }
463
590
  if (apiKey) {
464
591
  const expected = `Bearer ${apiKey}`;
465
592
  if (authHeader !== expected) {
@@ -9,6 +9,7 @@ import {
9
9
  removeKeys,
10
10
  } from "../config.mjs";
11
11
  import {
12
+ MEKO_MCP_USER_AGENT,
12
13
  getAssetRoot,
13
14
  getInstallerVersion,
14
15
  markerIsStale,
@@ -31,6 +32,7 @@ import {
31
32
  * "meko": {
32
33
  * "command": "npx",
33
34
  * "args": ["-y", "mcp-remote", "<url>", "--transport", "http-only",
35
+ * "--header", "User-Agent:meko-mcp-installer/1.0 (...)",
34
36
  * "--header", "Authorization:Bearer ${MEKO_API_KEY}"],
35
37
  * "env": { "MEKO_API_KEY": "<key>" }
36
38
  * }
@@ -122,8 +124,10 @@ export function createClaudeDesktopAdapter(options = {}) {
122
124
  mergeSettings(path, { mcpServers: { [name]: server } });
123
125
  },
124
126
  setHookEnvironment({ dryRun, log }) {
125
- // Claude Desktop has no hook API; the skill carries the proactive-memory
126
- // behavior instead. Nothing to write here.
127
+ // Claude Desktop has no hook API, so the skill mandates per-turn
128
+ // conversation_add_message posting (the server extracts memories from
129
+ // those posts); proactive memory_add is no longer the model. Nothing to
130
+ // write here.
127
131
  if (dryRun) {
128
132
  log.dim(" No hook environment required for Claude Desktop (skill-driven).");
129
133
  }
@@ -132,6 +136,8 @@ export function createClaudeDesktopAdapter(options = {}) {
132
136
  const assetRoot = assetRootResolver();
133
137
  const script = join(assetRoot, "scripts", "package-desktop-skill.sh");
134
138
  const output = join(assetRoot, "meko-mcp-tools-desktop.skill");
139
+ const selectOutput = join(assetRoot, "meko-select-datapack-desktop.skill");
140
+ const outputs = [output, selectOutput];
135
141
  // The .skill output is a ZIP file, so we can't drop a marker inside it.
136
142
  // Stamp the marker in the parent dir instead — it signals which
137
143
  // installer version produced the bundle sitting alongside it, so a
@@ -141,9 +147,11 @@ export function createClaudeDesktopAdapter(options = {}) {
141
147
 
142
148
  if (dryRun) {
143
149
  log.dim(` Would run: bash ${script}`);
144
- log.dim(` Would produce: ${output}`);
150
+ for (const skillOutput of outputs) {
151
+ log.dim(` Would produce: ${skillOutput}`);
152
+ }
145
153
  log.dim(` Would write .meko-installer marker (version=${currentVersion}) in ${markerDir}`);
146
- return { marketplace: "skipped", skills: "dry-run", output };
154
+ return { marketplace: "skipped", skills: "dry-run", output, outputs };
147
155
  }
148
156
 
149
157
  if (!existsSync(script)) {
@@ -158,7 +166,7 @@ export function createClaudeDesktopAdapter(options = {}) {
158
166
  // re-drag the .skill into Claude Desktop's Extensions panel; we can't do
159
167
  // that step for them. Keep rebuilding either way.
160
168
  const existingMarker = readInstallerMarker(markerDir);
161
- if (existsSync(output) && existingMarker && markerIsStale(markerDir, currentVersion)) {
169
+ if (outputs.some((skillOutput) => existsSync(skillOutput)) && existingMarker && markerIsStale(markerDir, currentVersion)) {
162
170
  log.warn(
163
171
  `Claude Desktop .skill bundle was produced by installer ${existingMarker.version}. ` +
164
172
  `Rebuilding — you will need to re-drag it into Claude Desktop > Settings > Extensions.`,
@@ -172,8 +180,9 @@ export function createClaudeDesktopAdapter(options = {}) {
172
180
  return { marketplace: "skipped", skills: "error", error: err.message };
173
181
  }
174
182
 
175
- if (!existsSync(output)) {
176
- log.warn(`Expected ${output} after packaging but it was not produced.`);
183
+ const missingOutputs = outputs.filter((skillOutput) => !existsSync(skillOutput));
184
+ if (missingOutputs.length) {
185
+ log.warn(`Expected skill bundle(s) were not produced: ${missingOutputs.join(", ")}`);
177
186
  return { marketplace: "skipped", skills: "error" };
178
187
  }
179
188
 
@@ -186,12 +195,14 @@ export function createClaudeDesktopAdapter(options = {}) {
186
195
  log.warn(`Could not write .skill installer marker: ${err.message}`);
187
196
  }
188
197
 
189
- log.info(`Built Meko skill bundle: ${output}`);
198
+ for (const skillOutput of outputs) {
199
+ log.info(`Built Meko skill bundle: ${skillOutput}`);
200
+ }
190
201
  log.info(
191
- "Install it in Claude Desktop: Settings > Extensions > Import skill, " +
192
- "or drag the .skill file onto the Claude Desktop window.",
202
+ "Install both in Claude Desktop: Settings > Extensions > Import skill, " +
203
+ "or drag each .skill file onto the Claude Desktop window.",
193
204
  );
194
- return { marketplace: "skipped", skills: "ok", output };
205
+ return { marketplace: "skipped", skills: "ok", output, outputs };
195
206
  },
196
207
  uninstall({ name }) {
197
208
  const results = [];
@@ -246,6 +257,12 @@ export function createClaudeDesktopAdapter(options = {}) {
246
257
  `Server "${serverName}" in ${path} is missing required "command" and "args" fields.`,
247
258
  };
248
259
  }
260
+ if (!entry.args.includes(`User-Agent:${MEKO_MCP_USER_AGENT}`)) {
261
+ return {
262
+ ok: false,
263
+ message: `Server "${serverName}" in ${path} is missing the User-Agent header required by the Cloud WAF.`,
264
+ };
265
+ }
249
266
 
250
267
  return { ok: true, message: `Server "${serverName}" is registered in ${path}.` };
251
268
  },
@@ -262,7 +279,15 @@ export function createClaudeDesktopAdapter(options = {}) {
262
279
  * @returns {{command: string, args: string[], env?: Record<string,string>}}
263
280
  */
264
281
  export function buildMcpRemoteEntry({ url, apiKey }) {
265
- const args = ["-y", "mcp-remote", url, "--transport", "http-only"];
282
+ const args = [
283
+ "-y",
284
+ "mcp-remote",
285
+ url,
286
+ "--transport",
287
+ "http-only",
288
+ "--header",
289
+ `User-Agent:${MEKO_MCP_USER_AGENT}`,
290
+ ];
266
291
  const entry = { command: "npx", args };
267
292
  if (apiKey) {
268
293
  // `Authorization:Bearer ${MEKO_API_KEY}` with no space around the colon