@yugabytedb/meko-mcp 1.0.8 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +181 -32
- package/assets/hooks-handlers/lib/capture.js +176 -77
- package/assets/hooks-handlers/lib/package.json +1 -0
- package/assets/hooks-handlers/test_single_conversation.sh +1 -1
- package/assets/skills/meko-mcp-tools/SKILL.md +167 -53
- package/assets/skills/meko-mcp-tools/references/_sections.md +1 -1
- package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +84 -22
- package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +89 -56
- package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +15 -7
- package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +17 -9
- package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +94 -5
- package/assets/skills/meko-mcp-tools/references/tools-overview.md +52 -37
- package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +108 -37
- package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +6 -10
- package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +30 -18
- package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +55 -69
- package/bin/create-meko-setup.mjs +147 -18
- package/lib/clients/claude-code.mjs +378 -68
- package/lib/clients/claude-desktop.mjs +35 -1
- package/lib/clients/codex.mjs +977 -0
- package/lib/clients/common.mjs +142 -1
- package/lib/clients/cursor.mjs +71 -3
- package/lib/clients/index.mjs +2 -0
- package/lib/config.mjs +30 -0
- package/lib/detect.mjs +52 -44
- package/lib/installer.mjs +9 -2
- package/lib/mcp-transport.mjs +61 -2
- package/lib/migrate/adapters/memory-md.mjs +13 -6
- package/lib/migrate/datapack.mjs +167 -0
- package/lib/migrate/id.mjs +127 -9
- package/lib/migrate/orchestrator.mjs +68 -10
- package/lib/migrate.mjs +346 -3
- package/lib/prompt.mjs +22 -4
- package/lib/validate.mjs +313 -9
- package/lib/version-check.mjs +178 -0
- package/package.json +2 -1
package/lib/clients/common.mjs
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
symlinkSync,
|
|
6
|
+
cpSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
unlinkSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
3
13
|
import { dirname, join, resolve } from "node:path";
|
|
4
14
|
import { fileURLToPath } from "node:url";
|
|
5
15
|
|
|
@@ -131,3 +141,134 @@ export function removePath(targetPath) {
|
|
|
131
141
|
// no-op
|
|
132
142
|
}
|
|
133
143
|
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Installer version markers
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
//
|
|
149
|
+
// Each artifact the installer owns (a deployed skill dir, a built .skill
|
|
150
|
+
// bundle, the Claude Code meko-capture dir, etc.) gets a `.meko-installer`
|
|
151
|
+
// marker file stamped with the installer's version. On re-run we compare the
|
|
152
|
+
// marker's version against the current installer to decide whether we need
|
|
153
|
+
// to force-refresh a managed artifact. Without a marker the artifact is
|
|
154
|
+
// treated as user-managed and left alone — we never clobber a dir we didn't
|
|
155
|
+
// produce ourselves.
|
|
156
|
+
//
|
|
157
|
+
// The marker format is deliberately plain text, one `key=value` per line,
|
|
158
|
+
// so a human inspecting it with `cat` can immediately see what produced
|
|
159
|
+
// the artifact and when.
|
|
160
|
+
|
|
161
|
+
const MARKER_FILENAME = ".meko-installer";
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Read the installer's package.json `version` field. Mirrors the read
|
|
165
|
+
* performed in bin/create-meko-setup.mjs:56-59 so the marker and the
|
|
166
|
+
* CLI's --version report agree.
|
|
167
|
+
*
|
|
168
|
+
* @returns {string}
|
|
169
|
+
*/
|
|
170
|
+
export function getInstallerVersion() {
|
|
171
|
+
const libDir = dirname(fileURLToPath(import.meta.url));
|
|
172
|
+
const pkgPath = resolve(libDir, "..", "..", "package.json");
|
|
173
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
174
|
+
return pkg.version;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Write a `.meko-installer` marker inside `dir`. Format is one `key=value`
|
|
179
|
+
* line per field, e.g.
|
|
180
|
+
*
|
|
181
|
+
* version=1.0.8
|
|
182
|
+
* installedAt=2026-04-30T14:22:11.123Z
|
|
183
|
+
*
|
|
184
|
+
* Extra key/value pairs can be appended via `extra`. `dir` must exist; this
|
|
185
|
+
* function will not create it (the caller has already done mkdir).
|
|
186
|
+
*
|
|
187
|
+
* @param {string} dir
|
|
188
|
+
* @param {{extra?: Record<string,string>}} [opts]
|
|
189
|
+
* @returns {{path: string, version: string, installedAt: string}}
|
|
190
|
+
*/
|
|
191
|
+
export function writeInstallerMarker(dir, { extra = {} } = {}) {
|
|
192
|
+
const version = getInstallerVersion();
|
|
193
|
+
const installedAt = new Date().toISOString();
|
|
194
|
+
const lines = [`version=${version}`, `installedAt=${installedAt}`];
|
|
195
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
196
|
+
lines.push(`${key}=${value}`);
|
|
197
|
+
}
|
|
198
|
+
const markerPath = join(dir, MARKER_FILENAME);
|
|
199
|
+
writeFileSync(markerPath, lines.join("\n") + "\n", "utf8");
|
|
200
|
+
return { path: markerPath, version, installedAt };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Parse a `.meko-installer` marker from `dir`. Returns null if the marker
|
|
205
|
+
* is missing or unreadable. Callers treat null as "not ours / user-managed".
|
|
206
|
+
*
|
|
207
|
+
* Marker format evolved across installer versions:
|
|
208
|
+
* - pre-1.0.9: a single line containing the word "managed". No version.
|
|
209
|
+
* - 1.0.9+: `key=value` lines, including `version=` and `installedAt=`.
|
|
210
|
+
*
|
|
211
|
+
* Both shapes indicate "we own this artifact"; the caller needs to know
|
|
212
|
+
* which shape it got so it can decide whether to force a refresh. Legacy
|
|
213
|
+
* markers have no version to compare against, so they are returned with
|
|
214
|
+
* `legacy: true` and no `version` — `markerIsStale` then treats them as
|
|
215
|
+
* stale (any current version mismatches) and triggers a refresh. That's
|
|
216
|
+
* exactly the behavior users hitting the "stale skill" bug from 1.0.8
|
|
217
|
+
* need on their next re-install.
|
|
218
|
+
*
|
|
219
|
+
* @param {string} dir
|
|
220
|
+
* @returns {{version?: string, installedAt?: string, legacy?: boolean, [k: string]: string|boolean|undefined}|null}
|
|
221
|
+
*/
|
|
222
|
+
export function readInstallerMarker(dir) {
|
|
223
|
+
const markerPath = join(dir, MARKER_FILENAME);
|
|
224
|
+
let raw;
|
|
225
|
+
try {
|
|
226
|
+
raw = readFileSync(markerPath, "utf8");
|
|
227
|
+
} catch {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
const out = {};
|
|
231
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
232
|
+
const trimmed = line.trim();
|
|
233
|
+
if (!trimmed) continue;
|
|
234
|
+
const eq = trimmed.indexOf("=");
|
|
235
|
+
if (eq <= 0) continue;
|
|
236
|
+
const key = trimmed.slice(0, eq).trim();
|
|
237
|
+
const value = trimmed.slice(eq + 1).trim();
|
|
238
|
+
if (!key) continue;
|
|
239
|
+
out[key] = value;
|
|
240
|
+
}
|
|
241
|
+
if (out.version) return out;
|
|
242
|
+
|
|
243
|
+
// Legacy marker path: pre-1.0.9 installs wrote the literal text
|
|
244
|
+
// `managed\n` (no `=`). Recognize it so we don't mistake an older
|
|
245
|
+
// managed dir for a user-managed one. Returning a parsed object with
|
|
246
|
+
// `legacy: true` lets markerIsStale do the right thing (stale → refresh).
|
|
247
|
+
if (raw.trim() === "managed") {
|
|
248
|
+
return { legacy: true };
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* True when the artifact at `dir` was produced by a different installer
|
|
255
|
+
* version than `currentVersion`. Also true when the marker is missing or
|
|
256
|
+
* unparseable — in that case the caller still needs to check whether the
|
|
257
|
+
* artifact dir exists (missing marker + missing dir = fresh install; missing
|
|
258
|
+
* marker + existing dir = user-managed, handle separately).
|
|
259
|
+
*
|
|
260
|
+
* Strict inequality by design — any version change forces a refresh, even
|
|
261
|
+
* a downgrade. We don't want to play semver-comparison games when the
|
|
262
|
+
* artifact content may have changed in either direction.
|
|
263
|
+
*
|
|
264
|
+
* @param {string} dir
|
|
265
|
+
* @param {string} currentVersion
|
|
266
|
+
* @returns {boolean}
|
|
267
|
+
*/
|
|
268
|
+
export function markerIsStale(dir, currentVersion) {
|
|
269
|
+
const marker = readInstallerMarker(dir);
|
|
270
|
+
if (!marker) return true;
|
|
271
|
+
// Legacy `managed\n` marker has no version recorded — always stale.
|
|
272
|
+
if (marker.legacy) return true;
|
|
273
|
+
return marker.version !== currentVersion;
|
|
274
|
+
}
|
package/lib/clients/cursor.mjs
CHANGED
|
@@ -2,7 +2,6 @@ import {
|
|
|
2
2
|
existsSync,
|
|
3
3
|
lstatSync,
|
|
4
4
|
readlinkSync,
|
|
5
|
-
writeFileSync,
|
|
6
5
|
mkdirSync,
|
|
7
6
|
} from "node:fs";
|
|
8
7
|
import { homedir } from "node:os";
|
|
@@ -17,10 +16,14 @@ import {
|
|
|
17
16
|
writeJsonFile,
|
|
18
17
|
} from "../config.mjs";
|
|
19
18
|
import {
|
|
19
|
+
getInstallerVersion,
|
|
20
|
+
markerIsStale,
|
|
21
|
+
readInstallerMarker,
|
|
20
22
|
resolveHooksRoot,
|
|
21
23
|
resolveSkillSource,
|
|
22
24
|
removePath,
|
|
23
25
|
symlinkOrCopy,
|
|
26
|
+
writeInstallerMarker,
|
|
24
27
|
} from "./common.mjs";
|
|
25
28
|
|
|
26
29
|
const SKILLS = [{ name: "meko-mcp-tools" }];
|
|
@@ -118,8 +121,10 @@ export function createCursorAdapter(customPaths = {}) {
|
|
|
118
121
|
},
|
|
119
122
|
installSkills({ dryRun, log, scope, projectRoot }) {
|
|
120
123
|
const skillDir = paths.getSkillsDir(scope, projectRoot);
|
|
124
|
+
const currentVersion = getInstallerVersion();
|
|
121
125
|
if (dryRun) {
|
|
122
126
|
log.dim(` Would install skills into ${skillDir}`);
|
|
127
|
+
log.dim(` Would write .meko-installer marker (version=${currentVersion})`);
|
|
123
128
|
return { marketplace: "skipped", skills: "dry-run" };
|
|
124
129
|
}
|
|
125
130
|
|
|
@@ -127,10 +132,42 @@ export function createCursorAdapter(customPaths = {}) {
|
|
|
127
132
|
for (const skill of SKILLS) {
|
|
128
133
|
const source = resolveSkillSource(skill.name);
|
|
129
134
|
const destination = join(skillDir, skill.name);
|
|
130
|
-
|
|
135
|
+
|
|
136
|
+
if (existsSync(destination)) {
|
|
137
|
+
// Symlinks pointing at our own source directory are already live —
|
|
138
|
+
// the source tree IS the latest content, so there's nothing to
|
|
139
|
+
// refresh. Treat them the same as a fresh-current-version install.
|
|
140
|
+
if (isOwnSymlink(destination, source)) continue;
|
|
141
|
+
|
|
142
|
+
const marker = readInstallerMarker(destination);
|
|
143
|
+
if (!marker) {
|
|
144
|
+
// No marker AND not a symlink we own — user-managed dir or a
|
|
145
|
+
// pre-marker install. Leave it alone to avoid clobbering local
|
|
146
|
+
// edits; the user can delete it themselves for a fresh deploy.
|
|
147
|
+
log.dim(
|
|
148
|
+
` Skipping ${destination}: no .meko-installer marker (user-managed).`,
|
|
149
|
+
);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (markerIsStale(destination, currentVersion)) {
|
|
153
|
+
const from = marker.version ?? "pre-1.0.9";
|
|
154
|
+
log.dim(
|
|
155
|
+
` Upgrading cursor skill from ${from} -> ${currentVersion}`,
|
|
156
|
+
);
|
|
157
|
+
removePath(destination);
|
|
158
|
+
} else {
|
|
159
|
+
// Same version already deployed — nothing to do.
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
131
164
|
const strategy = symlinkOrCopy(source, destination);
|
|
165
|
+
// Only stamp a marker when we materialized a real copy. Writing into
|
|
166
|
+
// a symlinked source dir would mutate the shipped asset tree, which
|
|
167
|
+
// is confusing at best and a permission error at worst. Symlinks
|
|
168
|
+
// re-resolve via isOwnSymlink on the next run.
|
|
132
169
|
if (strategy === "copy") {
|
|
133
|
-
|
|
170
|
+
writeInstallerMarker(destination);
|
|
134
171
|
}
|
|
135
172
|
}
|
|
136
173
|
|
|
@@ -217,6 +254,20 @@ function installNativeCursorHooks({ scope, projectRoot, url, apiKey, paths }) {
|
|
|
217
254
|
writeJsonFile(hooksPath, cfg);
|
|
218
255
|
}
|
|
219
256
|
|
|
257
|
+
// Cursor's claude-compat mode writes absolute-path Meko hook entries into
|
|
258
|
+
// the shared settings.json (on scope=user, the same file Claude Code owns)
|
|
259
|
+
// and de-dupes them via upsertClaudeCompatHook. The Claude Code adapter's
|
|
260
|
+
// pruneMekoHookEntries (issue #60) is narrowed to preserve any entry whose
|
|
261
|
+
// command references the current resolveHooksRoot() — so the entry this
|
|
262
|
+
// function writes survives a subsequent Claude Code install from the
|
|
263
|
+
// SAME install root.
|
|
264
|
+
//
|
|
265
|
+
// TRADE-OFF: if Cursor installed from a different root (e.g. repo clone)
|
|
266
|
+
// than a later Claude Code install (e.g. npx cache), Cursor's entries look
|
|
267
|
+
// byte-identical to the stale Claude Code entries issue #60 targets. They
|
|
268
|
+
// will be pruned; the user must re-run the Cursor installer to restore
|
|
269
|
+
// them. pruneMekoHookEntries emits a warning on any removal so the user
|
|
270
|
+
// has a chance to notice before hooks silently disappear.
|
|
220
271
|
function installClaudeCompatHooks({ scope, projectRoot, url, apiKey, paths }) {
|
|
221
272
|
const settingsPath = paths.getClaudeCompatSettingsPath(scope, projectRoot);
|
|
222
273
|
const cfg = readJsonFile(settingsPath);
|
|
@@ -314,6 +365,23 @@ function uninstallCursorHooks({ scope, projectRoot, mode, paths }) {
|
|
|
314
365
|
}
|
|
315
366
|
}
|
|
316
367
|
|
|
368
|
+
/**
|
|
369
|
+
* True when `destination` is a symlink that resolves to `expectedSource`.
|
|
370
|
+
* Used to recognize in-place symlink installs on re-run: the live source is
|
|
371
|
+
* the latest content, so there's nothing to refresh.
|
|
372
|
+
*/
|
|
373
|
+
function isOwnSymlink(destination, expectedSource) {
|
|
374
|
+
try {
|
|
375
|
+
const stat = lstatSync(destination);
|
|
376
|
+
if (!stat.isSymbolicLink()) return false;
|
|
377
|
+
const linkTarget = readlinkSync(destination);
|
|
378
|
+
const resolvedTarget = resolve(dirname(destination), linkTarget);
|
|
379
|
+
return resolvedTarget === expectedSource;
|
|
380
|
+
} catch {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
317
385
|
function uninstallCursorSkills({ scope, projectRoot, paths }) {
|
|
318
386
|
const skillsDir = paths.getSkillsDir(scope, projectRoot);
|
|
319
387
|
|
package/lib/clients/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createClaudeCodeAdapter } from "./claude-code.mjs";
|
|
2
2
|
import { createClaudeDesktopAdapter } from "./claude-desktop.mjs";
|
|
3
|
+
import { createCodexAdapter } from "./codex.mjs";
|
|
3
4
|
import { createCursorAdapter } from "./cursor.mjs";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -10,5 +11,6 @@ export function getClientAdapters() {
|
|
|
10
11
|
"claude-code": createClaudeCodeAdapter(),
|
|
11
12
|
"claude-desktop": createClaudeDesktopAdapter(),
|
|
12
13
|
cursor: createCursorAdapter(),
|
|
14
|
+
codex: createCodexAdapter(),
|
|
13
15
|
};
|
|
14
16
|
}
|
package/lib/config.mjs
CHANGED
|
@@ -96,6 +96,36 @@ export function getClaudeCompatSettingsPath(scope, projectRoot) {
|
|
|
96
96
|
return getClaudeCodeSettingsPath();
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Return path to Codex's user-level config (TOML).
|
|
101
|
+
* Honors $CODEX_HOME if set, else defaults to ~/.codex/config.toml.
|
|
102
|
+
* Codex has no project-level config — caller should warn and degrade
|
|
103
|
+
* if a project scope was requested.
|
|
104
|
+
* @returns {string}
|
|
105
|
+
*/
|
|
106
|
+
export function getCodexConfigPath() {
|
|
107
|
+
const codexHome = process.env.CODEX_HOME || join(homedir(), ".codex");
|
|
108
|
+
return join(codexHome, "config.toml");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Return path to Codex's user-level AGENTS.md.
|
|
113
|
+
* @returns {string}
|
|
114
|
+
*/
|
|
115
|
+
export function getCodexAgentsMdPath() {
|
|
116
|
+
const codexHome = process.env.CODEX_HOME || join(homedir(), ".codex");
|
|
117
|
+
return join(codexHome, "AGENTS.md");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Return path to the shared agent-skills directory used by Codex
|
|
122
|
+
* (and other agent runtimes that follow the convention).
|
|
123
|
+
* @returns {string}
|
|
124
|
+
*/
|
|
125
|
+
export function getCodexSkillsRoot() {
|
|
126
|
+
return join(homedir(), ".agents", "skills");
|
|
127
|
+
}
|
|
128
|
+
|
|
99
129
|
// ---------------------------------------------------------------------------
|
|
100
130
|
// Read / Write primitives
|
|
101
131
|
// ---------------------------------------------------------------------------
|
package/lib/detect.mjs
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { execSync } from "node:child_process";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
import { readJsonFile } from "./config.mjs";
|
|
11
15
|
|
|
12
16
|
// ---------------------------------------------------------------------------
|
|
13
17
|
// Internal helpers
|
|
@@ -70,56 +74,60 @@ export function detectClaude() {
|
|
|
70
74
|
// detectExistingMeko
|
|
71
75
|
// ---------------------------------------------------------------------------
|
|
72
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Canonical location of Claude Code's user-scope MCP registry. This is the
|
|
79
|
+
* file `claude mcp add` writes to — we read it directly rather than shelling
|
|
80
|
+
* out to `claude mcp list`, which takes 18–28s on real user machines and
|
|
81
|
+
* was timing out against our 10s guard (issue #80).
|
|
82
|
+
*
|
|
83
|
+
* @returns {string}
|
|
84
|
+
*/
|
|
85
|
+
export function getClaudeCodeConfigPath() {
|
|
86
|
+
return join(homedir(), ".claude.json");
|
|
87
|
+
}
|
|
88
|
+
|
|
73
89
|
/**
|
|
74
90
|
* Check if a Meko MCP server is already registered in Claude Code.
|
|
75
91
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
92
|
+
* Reads `~/.claude.json` directly and inspects either `mcpServers[serverName]`
|
|
93
|
+
* (user scope) or `projects[projectRoot].mcpServers[serverName]` (project
|
|
94
|
+
* scope). This mirrors the approach the Claude Desktop adapter already uses
|
|
95
|
+
* against its own config (installer/lib/clients/claude-desktop.mjs:detectExisting)
|
|
96
|
+
* and replaces the previous `claude mcp list` shell-out, which silently
|
|
97
|
+
* returned `found: false` whenever the CLI exceeded the 10s run() timeout.
|
|
98
|
+
*
|
|
99
|
+
* Scope handling: `claude mcp add --scope project` writes under
|
|
100
|
+
* `projects[<absolute projectRoot>].mcpServers`, not the top-level
|
|
101
|
+
* `mcpServers`. A project-scope re-install must detect the existing entry
|
|
102
|
+
* in that nested location, otherwise the overwrite prompt is skipped and
|
|
103
|
+
* `claude mcp add --scope project` rejects the duplicate with "already
|
|
104
|
+
* exists". User-scope installs read the top-level `mcpServers` object
|
|
105
|
+
* (which is where the CLI writes entries added with `--scope user`).
|
|
80
106
|
*
|
|
81
107
|
* @param {string} [serverName="meko"] - The MCP server name to look for.
|
|
108
|
+
* @param {{ configPath?: string, scope?: "user"|"project", projectRoot?: string }} [options]
|
|
109
|
+
* `configPath` — test-only injection point for the config file path; defaults to `~/.claude.json`.
|
|
110
|
+
* `scope` — "user" (default) or "project". Project scope looks under `projects[projectRoot].mcpServers`.
|
|
111
|
+
* `projectRoot` — absolute path used as the key into `projects[...]` when `scope === "project"`.
|
|
112
|
+
* Ignored for user scope. Must be supplied by the caller for project scope.
|
|
82
113
|
* @returns {{ found: boolean, status: string }}
|
|
83
|
-
* `found` - true if a
|
|
84
|
-
* `status` -
|
|
85
|
-
*
|
|
114
|
+
* `found` - true if a matching MCP entry exists at the scope-appropriate location.
|
|
115
|
+
* `status` - "Configured" when found, "" otherwise. (Kept as a string for
|
|
116
|
+
* API compatibility with the prior implementation; callers use
|
|
117
|
+
* this only for logging, never parsing.)
|
|
86
118
|
*/
|
|
87
|
-
export function detectExistingMeko(serverName = "meko") {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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;
|
|
119
|
+
export function detectExistingMeko(serverName = "meko", options = {}) {
|
|
120
|
+
const { scope = "user", projectRoot } = options;
|
|
121
|
+
const configPath = options.configPath ?? getClaudeCodeConfigPath();
|
|
122
|
+
const cfg = readJsonFile(configPath);
|
|
123
|
+
|
|
124
|
+
const entry =
|
|
125
|
+
scope === "project"
|
|
126
|
+
? cfg?.projects?.[projectRoot]?.mcpServers?.[serverName]
|
|
127
|
+
: cfg?.mcpServers?.[serverName];
|
|
128
|
+
|
|
129
|
+
if (!entry) {
|
|
130
|
+
return { found: false, status: "" };
|
|
122
131
|
}
|
|
123
|
-
|
|
124
|
-
return result;
|
|
132
|
+
return { found: true, status: "Configured" };
|
|
125
133
|
}
|
package/lib/installer.mjs
CHANGED
|
@@ -32,6 +32,7 @@ export async function install(options) {
|
|
|
32
32
|
name = "meko",
|
|
33
33
|
skipSkills = false,
|
|
34
34
|
skipValidation = false,
|
|
35
|
+
skipCanary = false,
|
|
35
36
|
dryRun = false,
|
|
36
37
|
verbose = false,
|
|
37
38
|
yes = false,
|
|
@@ -107,7 +108,7 @@ export async function install(options) {
|
|
|
107
108
|
|
|
108
109
|
if (!skipMcpRegistration) {
|
|
109
110
|
try {
|
|
110
|
-
adapter.registerServer({
|
|
111
|
+
await adapter.registerServer({
|
|
111
112
|
name,
|
|
112
113
|
url,
|
|
113
114
|
scope,
|
|
@@ -116,6 +117,7 @@ export async function install(options) {
|
|
|
116
117
|
verbose,
|
|
117
118
|
log: LOG,
|
|
118
119
|
projectRoot,
|
|
120
|
+
yes,
|
|
119
121
|
});
|
|
120
122
|
stepOk(currentStep, totalSteps, dryRun ? "Registering MCP server (dry-run)." : "MCP server registered.");
|
|
121
123
|
} catch (err) {
|
|
@@ -151,6 +153,7 @@ export async function install(options) {
|
|
|
151
153
|
if (!skipSkills) {
|
|
152
154
|
try {
|
|
153
155
|
const result = adapter.installSkills({
|
|
156
|
+
name,
|
|
154
157
|
scope,
|
|
155
158
|
dryRun,
|
|
156
159
|
verbose,
|
|
@@ -176,7 +179,8 @@ export async function install(options) {
|
|
|
176
179
|
try {
|
|
177
180
|
const results = await runValidation(url, apiKey, name, {
|
|
178
181
|
validateConfig: async ({ serverName }) =>
|
|
179
|
-
adapter.validateConfig({ serverName, scope, projectRoot }),
|
|
182
|
+
adapter.validateConfig({ serverName, scope, projectRoot, url, apiKey }),
|
|
183
|
+
skipCanary,
|
|
180
184
|
});
|
|
181
185
|
|
|
182
186
|
const checks = [
|
|
@@ -184,6 +188,9 @@ export async function install(options) {
|
|
|
184
188
|
{ label: "Connectivity", ...results.connectivity },
|
|
185
189
|
{ label: "Tools", ...results.tools },
|
|
186
190
|
];
|
|
191
|
+
if (results.canary) {
|
|
192
|
+
checks.push({ label: "Canary", ...results.canary });
|
|
193
|
+
}
|
|
187
194
|
for (const check of checks) {
|
|
188
195
|
if (check.ok) success(`${check.label}: ${check.message}`);
|
|
189
196
|
else warn(`${check.label}: ${check.message}`);
|
package/lib/mcp-transport.mjs
CHANGED
|
@@ -29,9 +29,16 @@ export function jsonRpcRequest(url, payload, apiKey, sessionId, opts = {}) {
|
|
|
29
29
|
const parsed = new URL(url);
|
|
30
30
|
const transport = parsed.protocol === "https:" ? https : http;
|
|
31
31
|
|
|
32
|
+
// MCP Streamable HTTP requires the client to advertise both content
|
|
33
|
+
// types: a non-streaming JSON response uses application/json, a
|
|
34
|
+
// streaming response (which is what FastMCP returns for `initialize`
|
|
35
|
+
// and any other call when stateless_http isn't `json_response=True`)
|
|
36
|
+
// uses text/event-stream. Sending only `application/json` produces a
|
|
37
|
+
// 406 Not Acceptable from spec-compliant servers (see meko-mcp-server
|
|
38
|
+
// PR #97 / MEKO-137 which turned this on for prod).
|
|
32
39
|
const headers = {
|
|
33
40
|
"Content-Type": "application/json",
|
|
34
|
-
Accept: "application/json",
|
|
41
|
+
Accept: "application/json, text/event-stream",
|
|
35
42
|
};
|
|
36
43
|
if (apiKey) {
|
|
37
44
|
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
@@ -56,10 +63,19 @@ export function jsonRpcRequest(url, payload, apiKey, sessionId, opts = {}) {
|
|
|
56
63
|
const chunks = [];
|
|
57
64
|
res.on("data", (chunk) => chunks.push(chunk));
|
|
58
65
|
res.on("end", () => {
|
|
66
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
67
|
+
// If the server answered with text/event-stream (the MCP spec's
|
|
68
|
+
// streaming response shape), extract the JSON-RPC payload from
|
|
69
|
+
// the first SSE `data:` field. Callers downstream want a plain
|
|
70
|
+
// JSON-RPC body — they don't care which framing the wire used.
|
|
71
|
+
const contentType = String(res.headers["content-type"] ?? "").toLowerCase();
|
|
72
|
+
const body = contentType.includes("text/event-stream")
|
|
73
|
+
? extractSseData(raw)
|
|
74
|
+
: raw;
|
|
59
75
|
resolve({
|
|
60
76
|
statusCode: res.statusCode,
|
|
61
77
|
headers: res.headers,
|
|
62
|
-
body
|
|
78
|
+
body,
|
|
63
79
|
});
|
|
64
80
|
});
|
|
65
81
|
},
|
|
@@ -75,6 +91,49 @@ export function jsonRpcRequest(url, payload, apiKey, sessionId, opts = {}) {
|
|
|
75
91
|
});
|
|
76
92
|
}
|
|
77
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Extract the JSON-RPC payload from an SSE-framed response body.
|
|
96
|
+
*
|
|
97
|
+
* MCP servers running Streamable HTTP wrap each JSON-RPC response in an
|
|
98
|
+
* SSE event of the form:
|
|
99
|
+
*
|
|
100
|
+
* event: message
|
|
101
|
+
* data: {"jsonrpc":"2.0","id":1,"result":{...}}
|
|
102
|
+
*
|
|
103
|
+
* For a single non-streaming JSON-RPC response (initialize, tools/list,
|
|
104
|
+
* tools/call) there's exactly one event in the body. SSE allows the
|
|
105
|
+
* payload to span multiple `data:` lines; per the spec they are joined
|
|
106
|
+
* with `\n`. We concatenate every `data:` line we see in the first event
|
|
107
|
+
* and return that. Returns the raw body unchanged if no `data:` line is
|
|
108
|
+
* found (defensive fallback so a misbehaving server still produces a
|
|
109
|
+
* useful "non-JSON response" error downstream rather than an empty
|
|
110
|
+
* string).
|
|
111
|
+
*
|
|
112
|
+
* @param {string} raw - Raw response body (already utf8-decoded).
|
|
113
|
+
* @returns {string}
|
|
114
|
+
*/
|
|
115
|
+
function extractSseData(raw) {
|
|
116
|
+
const dataLines = [];
|
|
117
|
+
// Split on all three SSE-permitted line terminators per the WHATWG SSE
|
|
118
|
+
// spec: LF, CRLF, and bare CR. Most servers (including FastMCP) emit
|
|
119
|
+
// LF or CRLF, but the spec is explicit about CR-only being valid and
|
|
120
|
+
// we shouldn't break on a producer that uses it.
|
|
121
|
+
for (const rawLine of raw.split(/\r\n|\r|\n/)) {
|
|
122
|
+
if (rawLine === "") {
|
|
123
|
+
// Blank line terminates the event. We only need the first event for
|
|
124
|
+
// a JSON-RPC response.
|
|
125
|
+
if (dataLines.length > 0) break;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
// `data:` field — capture the value, stripping the optional single
|
|
129
|
+
// leading space the spec allows after the colon. Anything else
|
|
130
|
+
// (`event:`, `id:`, `retry:`, comments starting with `:`) we ignore.
|
|
131
|
+
const match = /^data: ?(.*)$/.exec(rawLine);
|
|
132
|
+
if (match) dataLines.push(match[1]);
|
|
133
|
+
}
|
|
134
|
+
return dataLines.length > 0 ? dataLines.join("\n") : raw;
|
|
135
|
+
}
|
|
136
|
+
|
|
78
137
|
/**
|
|
79
138
|
* Produce a human-friendly message from a network error.
|
|
80
139
|
*
|
|
@@ -64,9 +64,9 @@ export async function enumerateMemoryItems(opts) {
|
|
|
64
64
|
if (!cwd) continue;
|
|
65
65
|
const agentId = agentIdForClaude(cwd);
|
|
66
66
|
const projMd = path.join(cwd, ".claude", "CLAUDE.md");
|
|
67
|
-
items.push(...readClaudeMd(projMd, agentId, "project"));
|
|
67
|
+
items.push(...readClaudeMd(projMd, agentId, "project", cwd));
|
|
68
68
|
const memDir = path.join(dir, "memory");
|
|
69
|
-
items.push(...readMemoryDir(memDir, agentId, "project"));
|
|
69
|
+
items.push(...readMemoryDir(memDir, agentId, "project", cwd));
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
} else {
|
|
@@ -74,10 +74,10 @@ export async function enumerateMemoryItems(opts) {
|
|
|
74
74
|
if (!cwd) return [];
|
|
75
75
|
const agentId = agentIdForClaude(cwd);
|
|
76
76
|
const projMd = path.join(cwd, ".claude", "CLAUDE.md");
|
|
77
|
-
items.push(...readClaudeMd(projMd, agentId, "project"));
|
|
77
|
+
items.push(...readClaudeMd(projMd, agentId, "project", cwd));
|
|
78
78
|
const slug = slugifyCwd(cwd);
|
|
79
79
|
const memDir = path.join(home, ".claude", "projects", slug, "memory");
|
|
80
|
-
items.push(...readMemoryDir(memDir, agentId, "project"));
|
|
80
|
+
items.push(...readMemoryDir(memDir, agentId, "project", cwd));
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
return items;
|
|
@@ -90,9 +90,13 @@ export async function enumerateMemoryItems(opts) {
|
|
|
90
90
|
* @param {string} filePath
|
|
91
91
|
* @param {string} agentId
|
|
92
92
|
* @param {"user"|"project"} scope
|
|
93
|
+
* @param {string|null} [cwd] - Absolute project path. Attached to metadata
|
|
94
|
+
* so downstream consumers can correlate even after the agent_id is
|
|
95
|
+
* sanitized down to a basename. Null for user-scope items (which use the
|
|
96
|
+
* `meko_agent` common bucket).
|
|
93
97
|
* @returns {MemoryItem[]}
|
|
94
98
|
*/
|
|
95
|
-
export function readClaudeMd(filePath, agentId, scope) {
|
|
99
|
+
export function readClaudeMd(filePath, agentId, scope, cwd = null) {
|
|
96
100
|
let raw;
|
|
97
101
|
try {
|
|
98
102
|
raw = fs.readFileSync(filePath, "utf8");
|
|
@@ -115,6 +119,7 @@ export function readClaudeMd(filePath, agentId, scope) {
|
|
|
115
119
|
source: path.basename(filePath),
|
|
116
120
|
scope,
|
|
117
121
|
sourcePath: filePath,
|
|
122
|
+
...(cwd ? { cwd } : {}),
|
|
118
123
|
...(heading ? { heading } : {}),
|
|
119
124
|
},
|
|
120
125
|
};
|
|
@@ -160,9 +165,10 @@ export function splitTopLevelSections(md) {
|
|
|
160
165
|
* @param {string} memDir
|
|
161
166
|
* @param {string} agentId
|
|
162
167
|
* @param {"user"|"project"} scope
|
|
168
|
+
* @param {string|null} [cwd] - Absolute project path; attached to metadata.
|
|
163
169
|
* @returns {MemoryItem[]}
|
|
164
170
|
*/
|
|
165
|
-
export function readMemoryDir(memDir, agentId, scope) {
|
|
171
|
+
export function readMemoryDir(memDir, agentId, scope, cwd = null) {
|
|
166
172
|
let entries;
|
|
167
173
|
try {
|
|
168
174
|
entries = fs.readdirSync(memDir);
|
|
@@ -193,6 +199,7 @@ export function readMemoryDir(memDir, agentId, scope) {
|
|
|
193
199
|
source: `memory/${entry}`,
|
|
194
200
|
scope,
|
|
195
201
|
sourcePath: full,
|
|
202
|
+
...(cwd ? { cwd } : {}),
|
|
196
203
|
...(frontmatter.name ? { name: frontmatter.name } : {}),
|
|
197
204
|
...(frontmatter.description ? { description: frontmatter.description } : {}),
|
|
198
205
|
...(frontmatter.type ? { type: frontmatter.type } : {}),
|