@yugabytedb/meko-mcp 1.0.3
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 +505 -0
- package/assets/hooks-handlers/lib/capture.js +863 -0
- package/assets/hooks-handlers/lib/checkpoint-timer.js +72 -0
- package/assets/hooks-handlers/pre-compact.sh +3 -0
- package/assets/hooks-handlers/session-end.sh +27 -0
- package/assets/hooks-handlers/session-start.sh +24 -0
- package/assets/hooks-handlers/test_single_conversation.sh +169 -0
- package/assets/skills/meko-mcp-tools/SKILL.md +192 -0
- package/assets/skills/meko-mcp-tools/references/_sections.md +31 -0
- package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +39 -0
- package/assets/skills/meko-mcp-tools/references/tools-conversation-dedup.md +49 -0
- package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +373 -0
- package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +56 -0
- package/assets/skills/meko-mcp-tools/references/tools-db-query-patterns.md +38 -0
- package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +61 -0
- package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +128 -0
- package/assets/skills/meko-mcp-tools/references/tools-overview.md +97 -0
- package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +63 -0
- package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +79 -0
- package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +143 -0
- package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +93 -0
- package/assets/skills/meko-mcp-tools/references/tools-write-guardrails.md +30 -0
- package/bin/create-meko-setup.mjs +499 -0
- package/lib/claude-code.mjs +14 -0
- package/lib/clients/claude-code.mjs +265 -0
- package/lib/clients/claude-desktop.mjs +256 -0
- package/lib/clients/common.mjs +107 -0
- package/lib/clients/cursor.mjs +332 -0
- package/lib/clients/index.mjs +14 -0
- package/lib/config.mjs +369 -0
- package/lib/detect.mjs +125 -0
- package/lib/installer.mjs +255 -0
- package/lib/logger.mjs +97 -0
- package/lib/mcp-transport.mjs +204 -0
- package/lib/migrate/adapters/claude-code.mjs +165 -0
- package/lib/migrate/adapters/cursor.mjs +221 -0
- package/lib/migrate/adapters/memory-md.mjs +230 -0
- package/lib/migrate/claude-parser.mjs +306 -0
- package/lib/migrate/daemon.mjs +67 -0
- package/lib/migrate/id.mjs +83 -0
- package/lib/migrate/lockfile.mjs +121 -0
- package/lib/migrate/logger.mjs +79 -0
- package/lib/migrate/mcp-client.mjs +224 -0
- package/lib/migrate/orchestrator.mjs +443 -0
- package/lib/migrate/progress.mjs +76 -0
- package/lib/migrate/state.mjs +177 -0
- package/lib/migrate.mjs +328 -0
- package/lib/prompt.mjs +212 -0
- package/lib/validate.mjs +259 -0
- package/package.json +49 -0
- package/scripts/bundle-assets.mjs +49 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { askYesNo } from "../prompt.mjs";
|
|
2
|
+
import {
|
|
3
|
+
getClaudeCodeSettingsPath,
|
|
4
|
+
mergeAllowList,
|
|
5
|
+
mergeSettings,
|
|
6
|
+
readJsonFile,
|
|
7
|
+
removeFromAllowList,
|
|
8
|
+
removeKeys,
|
|
9
|
+
writeJsonFile,
|
|
10
|
+
} from "../config.mjs";
|
|
11
|
+
import { detectClaude, detectExistingMeko } from "../detect.mjs";
|
|
12
|
+
import { redactKey, run } from "./common.mjs";
|
|
13
|
+
|
|
14
|
+
const CLAUDE_INSTALL_URL =
|
|
15
|
+
"https://docs.anthropic.com/en/docs/claude-code/getting-started";
|
|
16
|
+
|
|
17
|
+
// Hook scripts produced by earlier installer versions that may have had inline
|
|
18
|
+
// MEKO_MCP_URL=... / MEKO_API_KEY=... env assignments baked into the command
|
|
19
|
+
// string. Used by stripLiteralUrlFromHooks to recognize and clean them up.
|
|
20
|
+
const MEKO_HOOK_SCRIPTS = ["session-start.sh", "pre-compact.sh", "session-end.sh"];
|
|
21
|
+
|
|
22
|
+
// Meko MCP tools to union into `permissions.allow` so Claude Code resolves
|
|
23
|
+
// them without the deferred-tools ToolSearch round-trip (issue #5). The set
|
|
24
|
+
// is intentionally tight — only tools the agent calls proactively per
|
|
25
|
+
// the meko-mcp-tools skill — to avoid bloating every user's settings.
|
|
26
|
+
//
|
|
27
|
+
// NOTE: `permissions.allow` is verified to cover user-prompt bypass. Whether
|
|
28
|
+
// it also bypasses the deferred-tools schema-fetch gate must be re-verified
|
|
29
|
+
// empirically before this flag is considered load-bearing for #5. Tracked
|
|
30
|
+
// as Phase 10.1 in skills/TEST_PLAN.md.
|
|
31
|
+
const MEKO_ALWAYS_AVAILABLE_TOOLS = [
|
|
32
|
+
"mcp__meko__db_summarize_database",
|
|
33
|
+
"mcp__meko__memory_add",
|
|
34
|
+
"mcp__meko__memory_search",
|
|
35
|
+
"mcp__meko__memory_get_all",
|
|
36
|
+
"mcp__meko__memory_get_by_id",
|
|
37
|
+
"mcp__meko__memory_update",
|
|
38
|
+
"mcp__meko__memory_delete_by_id",
|
|
39
|
+
"mcp__meko__conversation_create",
|
|
40
|
+
"mcp__meko__conversation_add_message",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {{ getSettingsPath?: () => string }} [options]
|
|
45
|
+
* @returns {object}
|
|
46
|
+
*/
|
|
47
|
+
export function createClaudeCodeAdapter(options = {}) {
|
|
48
|
+
const resolveSettingsPath = options.getSettingsPath ?? getClaudeCodeSettingsPath;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
id: "claude-code",
|
|
52
|
+
displayName: "Claude Code",
|
|
53
|
+
settingsPath(scope, projectRoot) { // eslint-disable-line no-unused-vars
|
|
54
|
+
return resolveSettingsPath();
|
|
55
|
+
},
|
|
56
|
+
async detectPrerequisites({ verbose, log }) {
|
|
57
|
+
const claude = detectClaude();
|
|
58
|
+
if (!claude.found) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Claude Code CLI not found.\nInstall it first: ${CLAUDE_INSTALL_URL}`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (verbose) {
|
|
64
|
+
log.dim(` Claude CLI: ${claude.path} (${claude.version})`);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
detectExisting(name) {
|
|
68
|
+
return detectExistingMeko(name);
|
|
69
|
+
},
|
|
70
|
+
removeRegistration(name) {
|
|
71
|
+
run(`claude mcp remove ${name}`);
|
|
72
|
+
},
|
|
73
|
+
registerServer({ name, url, scope, apiKey, dryRun, verbose, log }) {
|
|
74
|
+
const cmd = this.buildRegisterCommand({ name, url, scope, apiKey });
|
|
75
|
+
if (dryRun) {
|
|
76
|
+
log.dim(` Would run: ${this.buildRegisterDisplayCommand({ name, url, scope, apiKey })}`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const output = this.runRegisterCommand(cmd);
|
|
80
|
+
if (verbose && output) {
|
|
81
|
+
const safeOutput = apiKey ? output.replaceAll(apiKey, "<REDACTED>") : output;
|
|
82
|
+
log.dim(` ${safeOutput}`);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
buildRegisterCommand({ name, url, scope, apiKey }) {
|
|
86
|
+
let cmd = `claude mcp add ${name} ${url} --transport http --scope ${scope}`;
|
|
87
|
+
if (apiKey) {
|
|
88
|
+
cmd += ` -H "API_KEY: ${apiKey}"`;
|
|
89
|
+
}
|
|
90
|
+
return cmd;
|
|
91
|
+
},
|
|
92
|
+
buildRegisterDisplayCommand({ name, url, scope, apiKey }) {
|
|
93
|
+
let displayCmd = `claude mcp add ${name} ${url} --transport http --scope ${scope}`;
|
|
94
|
+
if (apiKey) {
|
|
95
|
+
displayCmd += ` -H "API_KEY: ${redactKey(apiKey)}"`;
|
|
96
|
+
}
|
|
97
|
+
return displayCmd;
|
|
98
|
+
},
|
|
99
|
+
runRegisterCommand(cmd) {
|
|
100
|
+
return run(cmd);
|
|
101
|
+
},
|
|
102
|
+
async maybeConfirmOverwrite({ existingFound, name, yes }) {
|
|
103
|
+
if (!existingFound) return true;
|
|
104
|
+
if (yes) return true;
|
|
105
|
+
return askYesNo(
|
|
106
|
+
`${name} MCP is already configured. Overwrite?`,
|
|
107
|
+
false,
|
|
108
|
+
);
|
|
109
|
+
},
|
|
110
|
+
setHookEnvironment({ url, apiKey, dryRun, verbose, log, settingsPath }) {
|
|
111
|
+
const envPatch = { env: { MEKO_MCP_URL: url } };
|
|
112
|
+
if (apiKey) envPatch.env.MEKO_API_KEY = apiKey;
|
|
113
|
+
|
|
114
|
+
if (dryRun) {
|
|
115
|
+
log.dim(` Would set in ${settingsPath}:`);
|
|
116
|
+
log.dim(` MEKO_MCP_URL = ${url}`);
|
|
117
|
+
if (apiKey) {
|
|
118
|
+
log.dim(` MEKO_API_KEY = ${redactKey(apiKey)}`);
|
|
119
|
+
}
|
|
120
|
+
log.dim(
|
|
121
|
+
` Would union ${MEKO_ALWAYS_AVAILABLE_TOOLS.length} Meko tools into permissions.allow`,
|
|
122
|
+
);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
mergeSettings(settingsPath, envPatch);
|
|
127
|
+
mergeAllowList(settingsPath, "permissions.allow", MEKO_ALWAYS_AVAILABLE_TOOLS);
|
|
128
|
+
const migrated = stripLiteralUrlFromHooks(settingsPath);
|
|
129
|
+
if (verbose) {
|
|
130
|
+
log.dim(` Updated ${settingsPath}`);
|
|
131
|
+
log.dim(` MEKO_MCP_URL = ${url}`);
|
|
132
|
+
if (apiKey) {
|
|
133
|
+
log.dim(` MEKO_API_KEY = ${redactKey(apiKey)}`);
|
|
134
|
+
}
|
|
135
|
+
log.dim(
|
|
136
|
+
` permissions.allow += ${MEKO_ALWAYS_AVAILABLE_TOOLS.length} Meko tools`,
|
|
137
|
+
);
|
|
138
|
+
if (migrated) {
|
|
139
|
+
log.dim(" Migrated stale hook command literals to env-based form.");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
installSkills({ scope, dryRun, verbose, log }) {
|
|
144
|
+
const marketplaceCmd =
|
|
145
|
+
`claude plugin marketplace add yugabyte/meko-mcp-server --sparse skills --scope ${scope}`;
|
|
146
|
+
const pluginCmd = `claude plugin install meko-agent-skills --scope ${scope}`;
|
|
147
|
+
|
|
148
|
+
if (dryRun) {
|
|
149
|
+
log.dim(` Would run: ${marketplaceCmd}`);
|
|
150
|
+
log.dim(` Would run: ${pluginCmd}`);
|
|
151
|
+
return { marketplace: "dry-run", skills: "dry-run" };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let marketplaceStatus = "ok";
|
|
155
|
+
try {
|
|
156
|
+
const output = run(marketplaceCmd);
|
|
157
|
+
if (verbose && output) log.dim(` ${output}`);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
const msg = err.stderr || err.message || "";
|
|
160
|
+
if (msg.includes("already") || msg.includes("exists")) {
|
|
161
|
+
marketplaceStatus = "already";
|
|
162
|
+
} else {
|
|
163
|
+
marketplaceStatus = "error";
|
|
164
|
+
log.warn(`Non-critical: ${msg}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let skillStatus = "ok";
|
|
169
|
+
try {
|
|
170
|
+
const output = run(pluginCmd);
|
|
171
|
+
if (verbose && output) log.dim(` ${output}`);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
const msg = err.stderr || err.message || "";
|
|
174
|
+
if (msg.includes("already") || msg.includes("installed")) {
|
|
175
|
+
skillStatus = "already";
|
|
176
|
+
} else {
|
|
177
|
+
skillStatus = "error";
|
|
178
|
+
log.warn(`Non-critical: ${msg}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { marketplace: marketplaceStatus, skills: skillStatus };
|
|
183
|
+
},
|
|
184
|
+
uninstall({ name, settingsPath }) {
|
|
185
|
+
const results = [];
|
|
186
|
+
try {
|
|
187
|
+
run(`claude mcp remove ${name}`);
|
|
188
|
+
results.push({ key: "mcp", ok: true, message: `MCP server "${name}" removed.` });
|
|
189
|
+
} catch {
|
|
190
|
+
results.push({ key: "mcp", ok: false, message: `MCP server "${name}" was not registered (nothing to remove).` });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
removeKeys(settingsPath, ["env.MEKO_MCP_URL", "env.MEKO_API_KEY"]);
|
|
195
|
+
removeFromAllowList(settingsPath, "permissions.allow", MEKO_ALWAYS_AVAILABLE_TOOLS);
|
|
196
|
+
results.push({ key: "env", ok: true, message: "Environment variables and Meko allowlist removed from settings." });
|
|
197
|
+
} catch (err) {
|
|
198
|
+
results.push({ key: "env", ok: false, message: `Could not update settings: ${err.message}` });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
run("claude plugin uninstall meko-agent-skills");
|
|
203
|
+
results.push({ key: "skills", ok: true, message: "Skills plugin removed." });
|
|
204
|
+
} catch {
|
|
205
|
+
results.push({ key: "skills", ok: false, message: "Skills plugin was not installed (nothing to remove)." });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
run("claude plugin marketplace remove meko-agent-skills");
|
|
210
|
+
results.push({ key: "marketplace", ok: true, message: "Plugin marketplace entry removed." });
|
|
211
|
+
} catch {
|
|
212
|
+
results.push({ key: "marketplace", ok: false, message: "Plugin marketplace entry was not registered (nothing to remove)." });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return results;
|
|
216
|
+
},
|
|
217
|
+
async validateConfig({ serverName }) {
|
|
218
|
+
try {
|
|
219
|
+
const output = run("claude mcp list");
|
|
220
|
+
const lines = output.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
221
|
+
const found = lines.some((line) => line.split(":")[0].trim() === serverName);
|
|
222
|
+
if (found) return { ok: true, message: `Server "${serverName}" is registered.` };
|
|
223
|
+
return {
|
|
224
|
+
ok: false,
|
|
225
|
+
message: `Server "${serverName}" not found in \`claude mcp list\` output. Run the installer again or add it manually.`,
|
|
226
|
+
};
|
|
227
|
+
} catch (err) {
|
|
228
|
+
return { ok: false, message: `Failed to run \`claude mcp list\`: ${err.message}` };
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Rewrites `MEKO_MCP_URL='...' [MEKO_API_KEY='...'] bash "<script>"` hook command
|
|
235
|
+
// strings to `bash "<script>"`, for the three meko hook scripts. Earlier installer
|
|
236
|
+
// versions (cursor.mjs buildScriptCommand) literalized URL+key into the command,
|
|
237
|
+
// which silently shadowed the env block at runtime — see MEKO-20. Runs on every
|
|
238
|
+
// Claude Code install as a one-time migration. Returns true if anything changed.
|
|
239
|
+
export function stripLiteralUrlFromHooks(settingsPath) {
|
|
240
|
+
const cfg = readJsonFile(settingsPath);
|
|
241
|
+
if (!cfg?.hooks || typeof cfg.hooks !== "object") return false;
|
|
242
|
+
|
|
243
|
+
let changed = false;
|
|
244
|
+
for (const eventArr of Object.values(cfg.hooks)) {
|
|
245
|
+
if (!Array.isArray(eventArr)) continue;
|
|
246
|
+
for (const group of eventArr) {
|
|
247
|
+
if (!Array.isArray(group?.hooks)) continue;
|
|
248
|
+
for (const h of group.hooks) {
|
|
249
|
+
if (typeof h?.command !== "string") continue;
|
|
250
|
+
if (!MEKO_HOOK_SCRIPTS.some((s) => h.command.includes(s))) continue;
|
|
251
|
+
const rewritten = h.command.replace(
|
|
252
|
+
/^\s*(?:MEKO_\w+=(?:'[^']*'|"[^"]*"|\S+)\s+)+/,
|
|
253
|
+
"",
|
|
254
|
+
);
|
|
255
|
+
if (rewritten !== h.command) {
|
|
256
|
+
h.command = rewritten;
|
|
257
|
+
changed = true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (changed) writeJsonFile(settingsPath, cfg);
|
|
264
|
+
return changed;
|
|
265
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, statSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
getClaudeDesktopConfigPath,
|
|
7
|
+
mergeSettings,
|
|
8
|
+
readJsonFile,
|
|
9
|
+
removeKeys,
|
|
10
|
+
} from "../config.mjs";
|
|
11
|
+
import { getRepoRoot, run } from "./common.mjs";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Claude Desktop adapter.
|
|
15
|
+
*
|
|
16
|
+
* User-scope only. Writes to claude_desktop_config.json via mergeSettings so
|
|
17
|
+
* existing mcpServers entries (and unrelated preferences) are preserved and a
|
|
18
|
+
* .bak backup is left behind on every write.
|
|
19
|
+
*
|
|
20
|
+
* Claude Desktop does not load MCP servers via HTTP directly — its loader only
|
|
21
|
+
* spawns stdio subprocesses. We wrap the HTTP URL with the `mcp-remote` stdio
|
|
22
|
+
* bridge so Desktop treats it as a normal stdio server:
|
|
23
|
+
*
|
|
24
|
+
* "meko": {
|
|
25
|
+
* "command": "npx",
|
|
26
|
+
* "args": ["-y", "mcp-remote", "<url>", "--transport", "http-only",
|
|
27
|
+
* "--header", "API_KEY:${MEKO_API_KEY}"],
|
|
28
|
+
* "env": { "MEKO_API_KEY": "<key>" }
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
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.
|
|
34
|
+
*
|
|
35
|
+
* installSkills() runs skills/scripts/package-desktop-skill.sh to build the
|
|
36
|
+
* .skill bundle and prints import instructions. Desktop has no programmatic
|
|
37
|
+
* skill install API — the user drags the .skill into the app.
|
|
38
|
+
*/
|
|
39
|
+
export function createClaudeDesktopAdapter(options = {}) {
|
|
40
|
+
const configPathResolver = options.getConfigPath ?? getClaudeDesktopConfigPath;
|
|
41
|
+
const repoRootResolver = options.getRepoRoot ?? getRepoRoot;
|
|
42
|
+
const packageRunner = options.runPackageScript ?? defaultRunPackageScript;
|
|
43
|
+
const npxProber = options.probeNpx ?? defaultProbeNpx;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
id: "claude-desktop",
|
|
47
|
+
displayName: "Claude Desktop",
|
|
48
|
+
settingsPath() {
|
|
49
|
+
return configPathResolver();
|
|
50
|
+
},
|
|
51
|
+
async detectPrerequisites({ log, verbose } = {}) {
|
|
52
|
+
const npx = npxProber();
|
|
53
|
+
if (!npx.found) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
"npx (from Node.js) is required so Claude Desktop can launch the " +
|
|
56
|
+
"mcp-remote stdio bridge. Install Node.js 18+ (which includes npx) " +
|
|
57
|
+
"and re-run the installer.",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
if (verbose && log?.dim) {
|
|
61
|
+
log.dim(` npx: ${npx.path}${npx.version ? ` (${npx.version})` : ""}`);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
detectExisting(name) {
|
|
65
|
+
const cfg = readJsonFile(configPathResolver());
|
|
66
|
+
const entry = cfg?.mcpServers?.[name];
|
|
67
|
+
if (!entry) {
|
|
68
|
+
return { found: false, status: "" };
|
|
69
|
+
}
|
|
70
|
+
// If the existing entry's launcher path looks like a .mcpb extension
|
|
71
|
+
// install, surface that in the status so the installer prompts more
|
|
72
|
+
// loudly before overwriting — users who installed via Settings >
|
|
73
|
+
// Extensions usually don't expect `create-meko-setup` to touch the
|
|
74
|
+
// same config.
|
|
75
|
+
const argsJoined = Array.isArray(entry.args) ? entry.args.join(" ") : "";
|
|
76
|
+
const commandJoined = (entry.command || "") + " " + argsJoined;
|
|
77
|
+
const fromMcpb =
|
|
78
|
+
/\bClaude\b[^ ]*\bExtensions\b/i.test(commandJoined) ||
|
|
79
|
+
/mcp-remote-launcher\.js/.test(commandJoined);
|
|
80
|
+
return {
|
|
81
|
+
found: true,
|
|
82
|
+
status: fromMcpb ? "Configured (via .mcpb extension)" : "Configured",
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
removeRegistration(name) {
|
|
86
|
+
removeKeys(configPathResolver(), [`mcpServers.${name}`]);
|
|
87
|
+
},
|
|
88
|
+
registerServer({ name, url, apiKey, dryRun, log }) {
|
|
89
|
+
const path = configPathResolver();
|
|
90
|
+
const server = buildMcpRemoteEntry({ url, apiKey });
|
|
91
|
+
|
|
92
|
+
if (dryRun) {
|
|
93
|
+
log.dim(` Would update ${path}:`);
|
|
94
|
+
log.dim(` mcpServers.${name}.command = ${server.command}`);
|
|
95
|
+
log.dim(` mcpServers.${name}.args = ${JSON.stringify(server.args)}`);
|
|
96
|
+
if (apiKey) {
|
|
97
|
+
log.dim(` mcpServers.${name}.env.MEKO_API_KEY = <set>`);
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
mergeSettings(path, { mcpServers: { [name]: server } });
|
|
103
|
+
},
|
|
104
|
+
setHookEnvironment({ dryRun, log }) {
|
|
105
|
+
// Claude Desktop has no hook API; the skill carries the proactive-memory
|
|
106
|
+
// behavior instead. Nothing to write here.
|
|
107
|
+
if (dryRun) {
|
|
108
|
+
log.dim(" No hook environment required for Claude Desktop (skill-driven).");
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
installSkills({ dryRun, log }) {
|
|
112
|
+
const repoRoot = repoRootResolver();
|
|
113
|
+
const script = join(repoRoot, "skills", "scripts", "package-desktop-skill.sh");
|
|
114
|
+
const output = join(repoRoot, "skills", "meko-mcp-tools-desktop.skill");
|
|
115
|
+
|
|
116
|
+
if (dryRun) {
|
|
117
|
+
log.dim(` Would run: bash ${script}`);
|
|
118
|
+
log.dim(` Would produce: ${output}`);
|
|
119
|
+
return { marketplace: "skipped", skills: "dry-run", output };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!existsSync(script)) {
|
|
123
|
+
log.warn(
|
|
124
|
+
`Skill packaging script not found at ${script}. ` +
|
|
125
|
+
"Skipping .skill bundle build; install the skill manually via Claude Desktop settings.",
|
|
126
|
+
);
|
|
127
|
+
return { marketplace: "skipped", skills: "script-missing" };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
packageRunner(script);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
log.warn(`Skill packaging failed: ${err.message}`);
|
|
134
|
+
return { marketplace: "skipped", skills: "error", error: err.message };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!existsSync(output)) {
|
|
138
|
+
log.warn(`Expected ${output} after packaging but it was not produced.`);
|
|
139
|
+
return { marketplace: "skipped", skills: "error" };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
log.info(`Built Meko skill bundle: ${output}`);
|
|
143
|
+
log.info(
|
|
144
|
+
"Install it in Claude Desktop: Settings > Extensions > Import skill, " +
|
|
145
|
+
"or drag the .skill file onto the Claude Desktop window.",
|
|
146
|
+
);
|
|
147
|
+
return { marketplace: "skipped", skills: "ok", output };
|
|
148
|
+
},
|
|
149
|
+
uninstall({ name }) {
|
|
150
|
+
const results = [];
|
|
151
|
+
try {
|
|
152
|
+
this.removeRegistration(name);
|
|
153
|
+
results.push({
|
|
154
|
+
key: "mcp",
|
|
155
|
+
ok: true,
|
|
156
|
+
message: `Claude Desktop MCP server "${name}" removed from ${configPathResolver()}.`,
|
|
157
|
+
});
|
|
158
|
+
} catch (err) {
|
|
159
|
+
results.push({
|
|
160
|
+
key: "mcp",
|
|
161
|
+
ok: false,
|
|
162
|
+
message: `Could not remove Claude Desktop MCP server: ${err.message}`,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
results.push({
|
|
166
|
+
key: "skills",
|
|
167
|
+
ok: true,
|
|
168
|
+
message:
|
|
169
|
+
"Desktop skills are managed via the Desktop UI; " +
|
|
170
|
+
"remove the Meko skill manually if it was imported.",
|
|
171
|
+
});
|
|
172
|
+
return results;
|
|
173
|
+
},
|
|
174
|
+
async validateConfig({ serverName }) {
|
|
175
|
+
const path = configPathResolver();
|
|
176
|
+
const cfg = readJsonFile(path);
|
|
177
|
+
const entry = cfg?.mcpServers?.[serverName];
|
|
178
|
+
if (!entry) {
|
|
179
|
+
return { ok: false, message: `Server "${serverName}" not found in ${path}.` };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Claude Desktop only accepts stdio entries. A top-level `url` field
|
|
183
|
+
// (the HTTP shape) is silently rejected by Desktop's loader with a
|
|
184
|
+
// "not valid MCP server configurations" warning. Reject it here so the
|
|
185
|
+
// installer catches the regression before the user restarts the app.
|
|
186
|
+
if (entry.url && !entry.command) {
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
message:
|
|
190
|
+
`Server "${serverName}" in ${path} uses the unsupported HTTP shape ` +
|
|
191
|
+
`({ "url": ... }). Claude Desktop requires a stdio command entry. ` +
|
|
192
|
+
`Re-run the installer to rewrite it via mcp-remote.`,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (typeof entry.command !== "string" || !Array.isArray(entry.args)) {
|
|
196
|
+
return {
|
|
197
|
+
ok: false,
|
|
198
|
+
message:
|
|
199
|
+
`Server "${serverName}" in ${path} is missing required "command" and "args" fields.`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { ok: true, message: `Server "${serverName}" is registered in ${path}.` };
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Build a Claude Desktop mcpServers entry that wraps an HTTP MCP URL with the
|
|
210
|
+
* mcp-remote stdio bridge.
|
|
211
|
+
*
|
|
212
|
+
* Exported for tests.
|
|
213
|
+
*
|
|
214
|
+
* @param {{url: string, apiKey: string|null}} params
|
|
215
|
+
* @returns {{command: string, args: string[], env?: Record<string,string>}}
|
|
216
|
+
*/
|
|
217
|
+
export function buildMcpRemoteEntry({ url, apiKey }) {
|
|
218
|
+
const args = ["-y", "mcp-remote", url, "--transport", "http-only"];
|
|
219
|
+
const entry = { command: "npx", args };
|
|
220
|
+
if (apiKey) {
|
|
221
|
+
// `API_KEY:${MEKO_API_KEY}` with no space around the colon — mcp-remote
|
|
222
|
+
// docs call out a spacing bug in some clients' arg parsing. The header
|
|
223
|
+
// name matches what the Meko server expects (see capture.js mcpPost).
|
|
224
|
+
args.push("--header", "API_KEY:${MEKO_API_KEY}");
|
|
225
|
+
entry.env = { MEKO_API_KEY: apiKey };
|
|
226
|
+
}
|
|
227
|
+
return entry;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function defaultRunPackageScript(scriptPath) {
|
|
231
|
+
if (!statSync(scriptPath).isFile()) {
|
|
232
|
+
throw new Error(`Not a file: ${scriptPath}`);
|
|
233
|
+
}
|
|
234
|
+
execFileSync("bash", [scriptPath], { stdio: ["ignore", "pipe", "pipe"] });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Check whether `npx` is available on PATH.
|
|
239
|
+
* @returns {{found: boolean, path: string, version: string}}
|
|
240
|
+
*/
|
|
241
|
+
function defaultProbeNpx() {
|
|
242
|
+
const result = { found: false, path: "", version: "" };
|
|
243
|
+
try {
|
|
244
|
+
result.path = run("command -v npx");
|
|
245
|
+
} catch {
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
if (!result.path) return result;
|
|
249
|
+
try {
|
|
250
|
+
result.version = run("npx --version");
|
|
251
|
+
} catch {
|
|
252
|
+
// Path found but version probe failed — still usable.
|
|
253
|
+
}
|
|
254
|
+
result.found = true;
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, symlinkSync, cpSync, lstatSync, unlinkSync, rmSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @returns {string} Absolute repository root path.
|
|
8
|
+
*/
|
|
9
|
+
export function getRepoRoot() {
|
|
10
|
+
const libDir = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
return resolve(libDir, "..", "..", "..");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Locate the root of installable assets (skills, hook handlers).
|
|
16
|
+
*
|
|
17
|
+
* When the installer is published to npm, `prepack` copies the assets into
|
|
18
|
+
* `installer/assets/`; that path wins because it guarantees the shipped
|
|
19
|
+
* version of each skill/hook. When running from a source checkout without a
|
|
20
|
+
* packed `assets/` dir, fall back to the repo's `skills/` directory.
|
|
21
|
+
*
|
|
22
|
+
* Layout under both roots mirrors the source tree — callers use
|
|
23
|
+
* `resolveSkillSource`/`resolveHooksRoot` rather than building paths manually.
|
|
24
|
+
*
|
|
25
|
+
* @returns {string} Absolute path to the asset root.
|
|
26
|
+
*/
|
|
27
|
+
export function getAssetRoot() {
|
|
28
|
+
const libDir = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const packed = resolve(libDir, "..", "..", "assets");
|
|
30
|
+
if (existsSync(packed)) return packed;
|
|
31
|
+
return join(getRepoRoot(), "skills");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} name - skill directory name (e.g. "meko-mcp-tools")
|
|
36
|
+
* @returns {string} absolute path to the skill source directory
|
|
37
|
+
*/
|
|
38
|
+
export function resolveSkillSource(name) {
|
|
39
|
+
return join(getAssetRoot(), "skills", name);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @returns {string} absolute path to the hooks-handlers directory
|
|
44
|
+
*/
|
|
45
|
+
export function resolveHooksRoot() {
|
|
46
|
+
return join(getAssetRoot(), "hooks-handlers");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Run a shell command and return stdout.
|
|
51
|
+
* @param {string} cmd
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
export function run(cmd) {
|
|
55
|
+
return execSync(cmd, { encoding: "utf8", stdio: "pipe" }).trim();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {string|null|undefined} apiKey
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
export function redactKey(apiKey) {
|
|
63
|
+
return apiKey ? "<set>" : "<not set>";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Safely quote a string for shell usage.
|
|
68
|
+
* @param {string} value
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
export function shellQuote(value) {
|
|
72
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create symlink when possible; fallback to copy.
|
|
77
|
+
* @param {string} source
|
|
78
|
+
* @param {string} destination
|
|
79
|
+
* @returns {"symlink"|"copy"}
|
|
80
|
+
*/
|
|
81
|
+
export function symlinkOrCopy(source, destination) {
|
|
82
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
83
|
+
try {
|
|
84
|
+
symlinkSync(source, destination, "junction");
|
|
85
|
+
return "symlink";
|
|
86
|
+
} catch {
|
|
87
|
+
cpSync(source, destination, { recursive: true });
|
|
88
|
+
return "copy";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Remove path if it is a symlink or directory/file.
|
|
94
|
+
* @param {string} targetPath
|
|
95
|
+
*/
|
|
96
|
+
export function removePath(targetPath) {
|
|
97
|
+
try {
|
|
98
|
+
const stat = lstatSync(targetPath);
|
|
99
|
+
if (stat.isSymbolicLink()) {
|
|
100
|
+
unlinkSync(targetPath);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
rmSync(targetPath, { recursive: true, force: true });
|
|
104
|
+
} catch {
|
|
105
|
+
// no-op
|
|
106
|
+
}
|
|
107
|
+
}
|