@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.
Files changed (51) hide show
  1. package/README.md +505 -0
  2. package/assets/hooks-handlers/lib/capture.js +863 -0
  3. package/assets/hooks-handlers/lib/checkpoint-timer.js +72 -0
  4. package/assets/hooks-handlers/pre-compact.sh +3 -0
  5. package/assets/hooks-handlers/session-end.sh +27 -0
  6. package/assets/hooks-handlers/session-start.sh +24 -0
  7. package/assets/hooks-handlers/test_single_conversation.sh +169 -0
  8. package/assets/skills/meko-mcp-tools/SKILL.md +192 -0
  9. package/assets/skills/meko-mcp-tools/references/_sections.md +31 -0
  10. package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +39 -0
  11. package/assets/skills/meko-mcp-tools/references/tools-conversation-dedup.md +49 -0
  12. package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +373 -0
  13. package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +56 -0
  14. package/assets/skills/meko-mcp-tools/references/tools-db-query-patterns.md +38 -0
  15. package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +61 -0
  16. package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +128 -0
  17. package/assets/skills/meko-mcp-tools/references/tools-overview.md +97 -0
  18. package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +63 -0
  19. package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +79 -0
  20. package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +143 -0
  21. package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +93 -0
  22. package/assets/skills/meko-mcp-tools/references/tools-write-guardrails.md +30 -0
  23. package/bin/create-meko-setup.mjs +499 -0
  24. package/lib/claude-code.mjs +14 -0
  25. package/lib/clients/claude-code.mjs +265 -0
  26. package/lib/clients/claude-desktop.mjs +256 -0
  27. package/lib/clients/common.mjs +107 -0
  28. package/lib/clients/cursor.mjs +332 -0
  29. package/lib/clients/index.mjs +14 -0
  30. package/lib/config.mjs +369 -0
  31. package/lib/detect.mjs +125 -0
  32. package/lib/installer.mjs +255 -0
  33. package/lib/logger.mjs +97 -0
  34. package/lib/mcp-transport.mjs +204 -0
  35. package/lib/migrate/adapters/claude-code.mjs +165 -0
  36. package/lib/migrate/adapters/cursor.mjs +221 -0
  37. package/lib/migrate/adapters/memory-md.mjs +230 -0
  38. package/lib/migrate/claude-parser.mjs +306 -0
  39. package/lib/migrate/daemon.mjs +67 -0
  40. package/lib/migrate/id.mjs +83 -0
  41. package/lib/migrate/lockfile.mjs +121 -0
  42. package/lib/migrate/logger.mjs +79 -0
  43. package/lib/migrate/mcp-client.mjs +224 -0
  44. package/lib/migrate/orchestrator.mjs +443 -0
  45. package/lib/migrate/progress.mjs +76 -0
  46. package/lib/migrate/state.mjs +177 -0
  47. package/lib/migrate.mjs +328 -0
  48. package/lib/prompt.mjs +212 -0
  49. package/lib/validate.mjs +259 -0
  50. package/package.json +49 -0
  51. package/scripts/bundle-assets.mjs +49 -0
package/lib/detect.mjs ADDED
@@ -0,0 +1,125 @@
1
+ /**
2
+ * detect.mjs - Detection helpers for Claude CLI and existing Meko config.
3
+ *
4
+ * Uses child_process.execSync so the installer can fail fast if
5
+ * prerequisites are missing. Every function is safe to call on any
6
+ * platform - failures are caught and returned as structured results.
7
+ * Zero external dependencies - Node.js built-ins only.
8
+ */
9
+
10
+ import { execSync } from "node:child_process";
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Internal helpers
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /**
17
+ * Run a shell command and return its trimmed stdout.
18
+ * Returns `null` if the command fails for any reason.
19
+ *
20
+ * @param {string} cmd
21
+ * @returns {string | null}
22
+ */
23
+ function run(cmd) {
24
+ try {
25
+ return execSync(cmd, {
26
+ encoding: "utf8",
27
+ // Suppress stderr so failed commands don't litter the terminal.
28
+ stdio: ["pipe", "pipe", "pipe"],
29
+ // Short timeout to avoid hanging on unexpected interactive prompts.
30
+ timeout: 10_000,
31
+ }).trim();
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // detectClaude
39
+ // ---------------------------------------------------------------------------
40
+
41
+ /**
42
+ * Check whether the Claude CLI is installed and reachable on the PATH.
43
+ *
44
+ * @returns {{ found: boolean, version: string, path: string }}
45
+ * `found` - true when the `claude` binary exists and responds to --version.
46
+ * `version` - the version string reported by `claude --version`, or "".
47
+ * `path` - the absolute path to the binary, or "".
48
+ */
49
+ export function detectClaude() {
50
+ const result = { found: false, version: "", path: "" };
51
+
52
+ // Locate the binary.
53
+ const binPath = run("which claude");
54
+ if (!binPath) return result;
55
+
56
+ result.path = binPath;
57
+
58
+ // Retrieve version - `claude --version` typically outputs something like
59
+ // "1.0.12 (Claude Code)" or just a semver string.
60
+ const versionOutput = run("claude --version");
61
+ if (!versionOutput) return result;
62
+
63
+ result.version = versionOutput;
64
+ result.found = true;
65
+
66
+ return result;
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // detectExistingMeko
71
+ // ---------------------------------------------------------------------------
72
+
73
+ /**
74
+ * Check if a Meko MCP server is already registered in Claude Code.
75
+ *
76
+ * Runs `claude mcp list` and scans the output for the given server name.
77
+ * The output format may vary between Claude CLI versions; we look for a
78
+ * line that starts with `<serverName>:` (e.g. "meko: http://... - Connected")
79
+ * and extract a status keyword from it.
80
+ *
81
+ * @param {string} [serverName="meko"] - The MCP server name to look for.
82
+ * @returns {{ found: boolean, status: string }}
83
+ * `found` - true if a line matching `serverName` was found.
84
+ * `status` - the status value from that line (e.g. "Connected", "Failed"),
85
+ * or "" if not found.
86
+ */
87
+ export function detectExistingMeko(serverName = "meko") {
88
+ const result = { found: false, status: "" };
89
+
90
+ const output = run("claude mcp list");
91
+ if (!output) return result;
92
+
93
+ // Scan each line for one that begins with the server name.
94
+ const lines = output.split("\n");
95
+ for (const line of lines) {
96
+ const trimmed = line.trim();
97
+
98
+ // Match "serverName:" prefix (colon-delimited format) or a table row
99
+ // where the first whitespace-separated token equals the server name.
100
+ const isMatch =
101
+ trimmed.startsWith(`${serverName}:`) ||
102
+ trimmed.split(/\s+/)[0] === serverName;
103
+
104
+ if (!isMatch) continue;
105
+
106
+ result.found = true;
107
+
108
+ // Extract status from known keywords anywhere on the line.
109
+ if (/\bConnected\b/i.test(trimmed)) {
110
+ result.status = "Connected";
111
+ } else if (/\bFailed\b/i.test(trimmed)) {
112
+ result.status = "Failed";
113
+ } else if (/\bError\b/i.test(trimmed)) {
114
+ result.status = "Error";
115
+ } else if (/\bDisconnected\b/i.test(trimmed)) {
116
+ result.status = "Disconnected";
117
+ } else {
118
+ result.status = "Unknown";
119
+ }
120
+
121
+ return result;
122
+ }
123
+
124
+ return result;
125
+ }
@@ -0,0 +1,255 @@
1
+ import { askYesNo } from "./prompt.mjs";
2
+ import {
3
+ banner,
4
+ dim,
5
+ error,
6
+ info,
7
+ step,
8
+ stepFail,
9
+ stepOk,
10
+ success,
11
+ warn,
12
+ } from "./logger.mjs";
13
+ import { getClientAdapters } from "./clients/index.mjs";
14
+ import { redactKey } from "./clients/common.mjs";
15
+ import { runValidation } from "./validate.mjs";
16
+
17
+ const LOG = {
18
+ dim,
19
+ warn,
20
+ info,
21
+ };
22
+
23
+ /**
24
+ * Install and configure Meko MCP server for a selected client.
25
+ * @param {object} options
26
+ */
27
+ export async function install(options) {
28
+ const {
29
+ client = "claude-code",
30
+ url,
31
+ apiKey = null,
32
+ scope = "user",
33
+ name = "meko",
34
+ skipSkills = false,
35
+ skipValidation = false,
36
+ dryRun = false,
37
+ verbose = false,
38
+ yes = false,
39
+ projectRoot = process.cwd(),
40
+ cursorHookMode = "hybrid",
41
+ } = options;
42
+
43
+ const adapters = getClientAdapters();
44
+ const adapter = adapters[client];
45
+ if (!adapter) {
46
+ throw new Error(`Unsupported client "${client}".`);
47
+ }
48
+
49
+ const settingsPath = adapter.settingsPath(scope, projectRoot);
50
+
51
+ let totalSteps = 3; // register + env/hooks + validate
52
+ if (!skipSkills) totalSteps += 1;
53
+ if (skipValidation) totalSteps -= 1;
54
+
55
+ banner(`Meko Setup for ${adapter.displayName}`);
56
+ if (dryRun) warn("Dry-run mode - no changes will be made.");
57
+ if (verbose) {
58
+ dim(` client: ${client}`);
59
+ dim(` url: ${url}`);
60
+ dim(` apiKey: ${redactKey(apiKey)}`);
61
+ dim(` scope: ${scope}`);
62
+ dim(` name: ${name}`);
63
+ if (settingsPath) dim(` config: ${settingsPath}`);
64
+ }
65
+
66
+ info("Checking prerequisites...");
67
+ try {
68
+ await adapter.detectPrerequisites({ verbose, log: LOG, scope, projectRoot });
69
+ } catch (err) {
70
+ error(err.message);
71
+ throw err;
72
+ }
73
+ success(`${adapter.displayName} prerequisites detected.`);
74
+
75
+ const existing = adapter.detectExisting(name, { scope, projectRoot });
76
+ let skipMcpRegistration = false;
77
+ if (existing.found) {
78
+ let overwrite = true;
79
+ if (typeof adapter.maybeConfirmOverwrite === "function") {
80
+ overwrite = await adapter.maybeConfirmOverwrite({
81
+ existingFound: existing.found,
82
+ name,
83
+ yes,
84
+ });
85
+ } else if (!yes) {
86
+ overwrite = await askYesNo(
87
+ `Meko MCP is already configured for ${adapter.displayName}. Overwrite?`,
88
+ false,
89
+ );
90
+ }
91
+
92
+ if (!overwrite) {
93
+ warn("Skipping MCP registration (keeping existing config).");
94
+ skipMcpRegistration = true;
95
+ } else if (!dryRun) {
96
+ try {
97
+ adapter.removeRegistration(name, { scope, projectRoot });
98
+ if (verbose) dim(` Removed existing "${name}" MCP entry.`);
99
+ } catch {
100
+ // best effort
101
+ }
102
+ } else {
103
+ info(`Existing "${name}" MCP configuration found - overwriting.`);
104
+ }
105
+ }
106
+
107
+ let currentStep = 1;
108
+
109
+ if (!skipMcpRegistration) {
110
+ step(currentStep, totalSteps, "Registering MCP server...");
111
+ try {
112
+ adapter.registerServer({
113
+ name,
114
+ url,
115
+ scope,
116
+ apiKey,
117
+ dryRun,
118
+ verbose,
119
+ log: LOG,
120
+ projectRoot,
121
+ });
122
+ stepOk(currentStep, totalSteps, dryRun ? "Registering MCP server (dry-run)" : "MCP server registered.");
123
+ } catch (err) {
124
+ stepFail(currentStep, totalSteps, "Failed to register MCP server.");
125
+ error(err.stderr || err.message);
126
+ throw new Error("MCP server registration failed.");
127
+ }
128
+ } else {
129
+ step(currentStep, totalSteps, "Registering MCP server... skipped (existing config kept).");
130
+ }
131
+ currentStep++;
132
+
133
+ step(currentStep, totalSteps, "Configuring hook environment...");
134
+ try {
135
+ adapter.setHookEnvironment({
136
+ url,
137
+ apiKey,
138
+ scope,
139
+ dryRun,
140
+ verbose,
141
+ log: LOG,
142
+ settingsPath,
143
+ projectRoot,
144
+ cursorHookMode,
145
+ });
146
+ stepOk(currentStep, totalSteps, dryRun ? "Configuring hook environment (dry-run)" : "Hook environment configured.");
147
+ } catch (err) {
148
+ stepFail(currentStep, totalSteps, "Failed to configure hook environment.");
149
+ error(err.message);
150
+ throw new Error("Hook environment configuration failed.");
151
+ }
152
+ currentStep++;
153
+
154
+ if (!skipSkills) {
155
+ step(currentStep, totalSteps, "Installing skills support...");
156
+ try {
157
+ const result = adapter.installSkills({
158
+ scope,
159
+ dryRun,
160
+ verbose,
161
+ log: LOG,
162
+ projectRoot,
163
+ });
164
+ if (!dryRun && result?.marketplace === "manual") {
165
+ warn("Skills installation requires manual action for this client.");
166
+ }
167
+ stepOk(currentStep, totalSteps, dryRun ? "Installing skills support (dry-run)" : "Skills support configured.");
168
+ } catch (err) {
169
+ stepFail(currentStep, totalSteps, "Failed to configure skills support.");
170
+ warn(`Non-critical: ${err.message}`);
171
+ }
172
+ currentStep++;
173
+ }
174
+
175
+ if (!skipValidation) {
176
+ step(currentStep, totalSteps, "Validating setup...");
177
+ if (dryRun) {
178
+ dim(" Would validate config, MCP connectivity, and tool availability.");
179
+ stepOk(currentStep, totalSteps, "Validating setup (dry-run)");
180
+ } else {
181
+ try {
182
+ const results = await runValidation(url, apiKey, name, {
183
+ validateConfig: async ({ serverName }) =>
184
+ adapter.validateConfig({ serverName, scope, projectRoot }),
185
+ });
186
+
187
+ const checks = [
188
+ { label: "Config", ...results.config },
189
+ { label: "Connectivity", ...results.connectivity },
190
+ { label: "Tools", ...results.tools },
191
+ ];
192
+ for (const check of checks) {
193
+ if (check.ok) success(` ${check.label}: ${check.message}`);
194
+ else warn(` ${check.label}: ${check.message}`);
195
+ }
196
+
197
+ if (results.allPassed) {
198
+ stepOk(currentStep, totalSteps, "All validation checks passed.");
199
+ } else {
200
+ stepFail(currentStep, totalSteps, "Some validation checks failed.");
201
+ warn("Setup completed but some checks did not pass. See details above.");
202
+ }
203
+ } catch (err) {
204
+ stepFail(currentStep, totalSteps, "Validation error.");
205
+ warn(`Validation failed: ${err.message}`);
206
+ }
207
+ }
208
+ }
209
+
210
+ console.log("");
211
+ success("Meko MCP Server setup complete.");
212
+ info(`Restart ${adapter.displayName} to activate the updated configuration.`);
213
+ }
214
+
215
+ /**
216
+ * Remove installer-managed Meko configuration for selected client.
217
+ * @param {object} options
218
+ */
219
+ export async function uninstall(options = {}) {
220
+ const {
221
+ client = "claude-code",
222
+ name = "meko",
223
+ scope = "user",
224
+ verbose = false,
225
+ projectRoot = process.cwd(),
226
+ cursorHookMode = "hybrid",
227
+ } = options;
228
+
229
+ const adapters = getClientAdapters();
230
+ const adapter = adapters[client];
231
+ if (!adapter) {
232
+ throw new Error(`Unsupported client "${client}".`);
233
+ }
234
+
235
+ banner(`Meko Uninstall (${adapter.displayName})`);
236
+ const settingsPath = adapter.settingsPath(scope, projectRoot);
237
+ const results = adapter.uninstall({
238
+ name,
239
+ scope,
240
+ projectRoot,
241
+ settingsPath,
242
+ cursorHookMode,
243
+ });
244
+
245
+ for (const result of results) {
246
+ if (result.ok) success(result.message);
247
+ else warn(result.message);
248
+ }
249
+
250
+ console.log("");
251
+ success(`Meko has been uninstalled for ${adapter.displayName}.`);
252
+ if (verbose && settingsPath) {
253
+ dim(`Config path: ${settingsPath}`);
254
+ }
255
+ }
package/lib/logger.mjs ADDED
@@ -0,0 +1,97 @@
1
+ /**
2
+ * logger.mjs - Console output helpers with ANSI colors.
3
+ *
4
+ * Respects the NO_COLOR convention (https://no-color.org/).
5
+ * Zero external dependencies - Node.js built-ins only.
6
+ */
7
+
8
+ const useColor = !process.env.NO_COLOR;
9
+
10
+ // ANSI codes, collapsed to empty strings when color is suppressed.
11
+ const RESET = useColor ? "\x1b[0m" : "";
12
+ const BLUE = useColor ? "\x1b[34m" : "";
13
+ const GREEN = useColor ? "\x1b[32m" : "";
14
+ const YELLOW = useColor ? "\x1b[33m" : "";
15
+ const RED = useColor ? "\x1b[31m" : "";
16
+ const GRAY = useColor ? "\x1b[90m" : "";
17
+ const BOLD = useColor ? "\x1b[1m" : "";
18
+
19
+ // Unicode symbols with plain-text fallbacks when color is off.
20
+ const CHECK = useColor ? "\u2713" : "OK";
21
+ const CROSS = useColor ? "\u2717" : "FAIL";
22
+ const BULLET = useColor ? "\u2022" : "*";
23
+
24
+ /**
25
+ * Print an informational message.
26
+ * Example: ℹ Checking prerequisites...
27
+ */
28
+ export function info(msg) {
29
+ console.log(`${BLUE}${BULLET}${RESET} ${msg}`);
30
+ }
31
+
32
+ /**
33
+ * Print a success message.
34
+ * Example: ✓ Server registered
35
+ */
36
+ export function success(msg) {
37
+ console.log(`${GREEN}${CHECK}${RESET} ${msg}`);
38
+ }
39
+
40
+ /**
41
+ * Print a warning message.
42
+ * Example: ⚠ Config already exists
43
+ */
44
+ export function warn(msg) {
45
+ console.log(`${YELLOW}!${RESET} ${msg}`);
46
+ }
47
+
48
+ /**
49
+ * Print an error message.
50
+ * Example: ✗ Connection failed
51
+ */
52
+ export function error(msg) {
53
+ console.error(`${RED}${CROSS}${RESET} ${msg}`);
54
+ }
55
+
56
+ /**
57
+ * Print a numbered step that is in progress.
58
+ * Example: [1/5] Registering MCP server...
59
+ */
60
+ export function step(n, total, msg) {
61
+ console.log(` ${BLUE}[${n}/${total}]${RESET} ${msg}`);
62
+ }
63
+
64
+ /**
65
+ * Print a numbered step that succeeded.
66
+ * Example: [1/5] Registering MCP server... ✓
67
+ */
68
+ export function stepOk(n, total, msg) {
69
+ console.log(` ${GREEN}${CHECK}${RESET} ${BLUE}[${n}/${total}]${RESET} ${msg}`);
70
+ }
71
+
72
+ /**
73
+ * Print a numbered step that failed.
74
+ * Example: [3/5] Registering plugin marketplace... ✗ Failed: ...
75
+ */
76
+ export function stepFail(n, total, msg) {
77
+ console.log(` ${RED}${CROSS}${RESET} ${BLUE}[${n}/${total}]${RESET} ${msg}`);
78
+ }
79
+
80
+ /**
81
+ * Print a prominent banner / header line.
82
+ * Example:
83
+ * ━━━ Meko Setup for Claude Code ━━━
84
+ */
85
+ export function banner(title) {
86
+ const rule = useColor ? "\u2501".repeat(3) : "---";
87
+ console.log("");
88
+ console.log(`${BOLD}${BLUE}${rule} ${title} ${rule}${RESET}`);
89
+ console.log("");
90
+ }
91
+
92
+ /**
93
+ * Print dimmed / secondary text (e.g. URLs, paths, hints).
94
+ */
95
+ export function dim(msg) {
96
+ console.log(`${GRAY}${msg}${RESET}`);
97
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * mcp-transport.mjs — Shared Streamable-HTTP JSON-RPC client for Meko MCP.
3
+ *
4
+ * Used by validate.mjs (post-install validation) and migrate/mcp-client.mjs
5
+ * (background migration). Tracks the `Mcp-Session-Id` response header across
6
+ * requests within a session. Zero external dependencies.
7
+ */
8
+
9
+ import http from "node:http";
10
+ import https from "node:https";
11
+
12
+ /** Default per-request timeout in milliseconds. */
13
+ export const DEFAULT_REQUEST_TIMEOUT_MS = 10_000;
14
+
15
+ /**
16
+ * Send a JSON-RPC payload over HTTP(S).
17
+ *
18
+ * @param {string} url - Target URL.
19
+ * @param {object} payload - JSON-RPC body.
20
+ * @param {string|null} apiKey - Optional API key, sent as `API_KEY` header.
21
+ * @param {string|null} sessionId - Optional `Mcp-Session-Id` to include.
22
+ * @param {object} [opts]
23
+ * @param {number} [opts.timeoutMs] - Override per-request timeout.
24
+ * @returns {Promise<{statusCode: number, headers: object, body: string}>}
25
+ */
26
+ export function jsonRpcRequest(url, payload, apiKey, sessionId, opts = {}) {
27
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
28
+ return new Promise((resolve, reject) => {
29
+ const parsed = new URL(url);
30
+ const transport = parsed.protocol === "https:" ? https : http;
31
+
32
+ const headers = {
33
+ "Content-Type": "application/json",
34
+ Accept: "application/json",
35
+ };
36
+ if (apiKey) {
37
+ headers["API_KEY"] = apiKey;
38
+ }
39
+ if (sessionId) {
40
+ headers["Mcp-Session-Id"] = sessionId;
41
+ }
42
+
43
+ const body = JSON.stringify(payload);
44
+ headers["Content-Length"] = Buffer.byteLength(body);
45
+
46
+ const req = transport.request(
47
+ {
48
+ hostname: parsed.hostname,
49
+ port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
50
+ path: parsed.pathname + parsed.search,
51
+ method: "POST",
52
+ headers,
53
+ timeout: timeoutMs,
54
+ },
55
+ (res) => {
56
+ const chunks = [];
57
+ res.on("data", (chunk) => chunks.push(chunk));
58
+ res.on("end", () => {
59
+ resolve({
60
+ statusCode: res.statusCode,
61
+ headers: res.headers,
62
+ body: Buffer.concat(chunks).toString("utf8"),
63
+ });
64
+ });
65
+ },
66
+ );
67
+
68
+ req.on("timeout", () => {
69
+ req.destroy();
70
+ reject(new Error("TIMEOUT"));
71
+ });
72
+ req.on("error", reject);
73
+ req.write(body);
74
+ req.end();
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Produce a human-friendly message from a network error.
80
+ *
81
+ * @param {Error} err
82
+ * @param {number} [timeoutMs] - Included in timeout message.
83
+ * @returns {string}
84
+ */
85
+ export function classifyError(err, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
86
+ if (err.message === "TIMEOUT") {
87
+ return (
88
+ "Request timed out after " +
89
+ timeoutMs / 1000 +
90
+ "s. Is the server running and reachable?"
91
+ );
92
+ }
93
+ if (err.code === "ECONNREFUSED") {
94
+ return (
95
+ "Connection refused. Is the MCP server running? " +
96
+ "For local Meko, ensure `docker compose up -d` has started the stack."
97
+ );
98
+ }
99
+ if (err.code === "ENOTFOUND") {
100
+ return "DNS lookup failed for host. Check the URL is correct.";
101
+ }
102
+ if (err.code === "ECONNRESET") {
103
+ return "Connection was reset by the server.";
104
+ }
105
+ return err.message;
106
+ }
107
+
108
+ /**
109
+ * Perform the MCP `initialize` + `notifications/initialized` handshake.
110
+ * Returns the session id assigned by the server (or null if none).
111
+ *
112
+ * @param {string} url
113
+ * @param {string|null} apiKey
114
+ * @param {object} [opts]
115
+ * @param {string} [opts.clientName]
116
+ * @param {string} [opts.clientVersion]
117
+ * @param {string} [opts.protocolVersion]
118
+ * @param {number} [opts.timeoutMs]
119
+ * @returns {Promise<{ok: boolean, sessionId: string|null, statusCode?: number, message?: string, raw?: object}>}
120
+ */
121
+ export async function mcpInitialize(url, apiKey, opts = {}) {
122
+ const initPayload = {
123
+ jsonrpc: "2.0",
124
+ id: 1,
125
+ method: "initialize",
126
+ params: {
127
+ protocolVersion: opts.protocolVersion ?? "2025-03-26",
128
+ capabilities: {},
129
+ clientInfo: {
130
+ name: opts.clientName ?? "meko-installer",
131
+ version: opts.clientVersion ?? "1.0.0",
132
+ },
133
+ },
134
+ };
135
+
136
+ let res;
137
+ try {
138
+ res = await jsonRpcRequest(url, initPayload, apiKey, null, opts);
139
+ } catch (err) {
140
+ return {
141
+ ok: false,
142
+ sessionId: null,
143
+ message: classifyError(err, opts.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS),
144
+ };
145
+ }
146
+
147
+ if (res.statusCode === 401 || res.statusCode === 403) {
148
+ return {
149
+ ok: false,
150
+ sessionId: null,
151
+ statusCode: res.statusCode,
152
+ message:
153
+ `Server returned ${res.statusCode}. ` +
154
+ "Verify your API key is correct and has not expired.",
155
+ };
156
+ }
157
+
158
+ if (res.statusCode < 200 || res.statusCode >= 300) {
159
+ return {
160
+ ok: false,
161
+ sessionId: null,
162
+ statusCode: res.statusCode,
163
+ message: `Server returned HTTP ${res.statusCode}: ${res.body}`,
164
+ };
165
+ }
166
+
167
+ let parsed;
168
+ try {
169
+ parsed = JSON.parse(res.body);
170
+ } catch {
171
+ return {
172
+ ok: false,
173
+ sessionId: null,
174
+ statusCode: res.statusCode,
175
+ message: `Server returned non-JSON response: ${res.body.slice(0, 200)}`,
176
+ };
177
+ }
178
+
179
+ if (parsed.error) {
180
+ return {
181
+ ok: false,
182
+ sessionId: null,
183
+ statusCode: res.statusCode,
184
+ message: `Server error: ${parsed.error.message ?? JSON.stringify(parsed.error)}`,
185
+ };
186
+ }
187
+
188
+ const sessionId = res.headers["mcp-session-id"] ?? null;
189
+
190
+ // Fire-and-forget initialized notification.
191
+ try {
192
+ await jsonRpcRequest(
193
+ url,
194
+ { jsonrpc: "2.0", method: "notifications/initialized" },
195
+ apiKey,
196
+ sessionId,
197
+ opts,
198
+ );
199
+ } catch {
200
+ // Non-fatal: some transports accept this silently.
201
+ }
202
+
203
+ return { ok: true, sessionId, statusCode: res.statusCode, raw: parsed };
204
+ }