codex-dev-mcp-suite 1.0.0 → 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/.env.example +28 -7
- package/CHANGELOG.md +34 -0
- package/README.md +59 -10
- package/devjournal/env.js +3 -0
- package/devjournal/provider-chain.js +56 -0
- package/devjournal/rerank.js +27 -12
- package/devjournal/server.js +2 -1
- package/docs/clients/claude-code.md +46 -0
- package/docs/clients/codex.md +39 -0
- package/docs/clients/generic-mcp.md +45 -0
- package/docs/configuration.md +109 -0
- package/docs/privacy.md +39 -0
- package/package.json +17 -8
- package/project-memory/embedding.js +7 -6
- package/project-memory/env.js +3 -0
- package/project-memory/provider-chain.js +56 -0
- package/project-memory/rerank.js +27 -12
- package/project-memory/server.js +4 -2
- package/_testkit/harness.mjs +0 -126
- package/checkpoint/test.mjs +0 -73
- package/context-pack/test.mjs +0 -66
- package/devjournal/test.mjs +0 -68
- package/project-memory/test.mjs +0 -72
- package/project-memory/test.rerank.mjs +0 -53
- package/run-tests.mjs +0 -42
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-dev-mcp-suite",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Four local, file-based MCP servers for solo devs/vibecoders: persistent project memory, session handoff/resume, git-independent file checkpoints, and token-efficient project briefings. Works with any MCP client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,23 +43,32 @@
|
|
|
43
43
|
"files": [
|
|
44
44
|
"bin/*.mjs",
|
|
45
45
|
"project-memory/*.js",
|
|
46
|
-
"project-memory
|
|
46
|
+
"project-memory/server.js",
|
|
47
|
+
"project-memory/embedding.js",
|
|
48
|
+
"project-memory/env.js",
|
|
49
|
+
"project-memory/provider-chain.js",
|
|
50
|
+
"project-memory/rerank.js",
|
|
47
51
|
"project-memory/package.json",
|
|
48
52
|
"devjournal/*.js",
|
|
49
|
-
"devjournal
|
|
53
|
+
"devjournal/server.js",
|
|
54
|
+
"devjournal/env.js",
|
|
55
|
+
"devjournal/provider-chain.js",
|
|
56
|
+
"devjournal/rerank.js",
|
|
50
57
|
"devjournal/package.json",
|
|
51
58
|
"checkpoint/*.js",
|
|
52
|
-
"checkpoint
|
|
59
|
+
"checkpoint/server.js",
|
|
53
60
|
"checkpoint/package.json",
|
|
54
61
|
"context-pack/*.js",
|
|
55
|
-
"context-pack
|
|
62
|
+
"context-pack/server.js",
|
|
56
63
|
"context-pack/package.json",
|
|
57
|
-
"_testkit/*.mjs",
|
|
58
|
-
"run-tests.mjs",
|
|
59
64
|
"backfill-sessions-v2.mjs",
|
|
65
|
+
"docs/configuration.md",
|
|
66
|
+
"docs/privacy.md",
|
|
67
|
+
"docs/clients/*.md",
|
|
60
68
|
"CODEX_DEV_MCP_GUIDE.md",
|
|
69
|
+
"CHANGELOG.md",
|
|
61
70
|
"README.md",
|
|
62
71
|
"LICENSE",
|
|
63
72
|
".env.example"
|
|
64
73
|
]
|
|
65
|
-
}
|
|
74
|
+
}
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Embedding helper for
|
|
2
|
+
* Embedding helper for any OpenAI-compatible /v1/embeddings endpoint.
|
|
3
3
|
* Degrades gracefully: returns null on any failure so callers fall back
|
|
4
4
|
* to keyword search. Never throws.
|
|
5
5
|
*/
|
|
6
6
|
import http from "http";
|
|
7
7
|
import https from "https";
|
|
8
8
|
import { URL } from "url";
|
|
9
|
+
import { deterministicEnabled } from "./env.js";
|
|
9
10
|
|
|
10
|
-
const BASE = process.env.LLM_BASE_URL || process.env.EMBED_URL || process.env.NINEROUTER_URL || "http://localhost:20128";
|
|
11
|
-
const KEY = process.env.LLM_API_KEY || process.env.EMBED_KEY || process.env.NINEROUTER_KEY || "";
|
|
12
|
-
const MODEL = process.env.EMBED_MODEL || "bm/baai/bge-m3";
|
|
11
|
+
const BASE = process.env.MCP_EMBED_BASE_URL || process.env.MCP_LLM_BASE_URL || process.env.LLM_BASE_URL || process.env.EMBED_URL || process.env.NINEROUTER_URL || "http://localhost:20128";
|
|
12
|
+
const KEY = process.env.MCP_EMBED_API_KEY || process.env.MCP_LLM_API_KEY || process.env.LLM_API_KEY || process.env.EMBED_KEY || process.env.NINEROUTER_KEY || "";
|
|
13
|
+
const MODEL = process.env.MCP_EMBED_MODEL || process.env.EMBED_MODEL || "bm/baai/bge-m3";
|
|
13
14
|
const TIMEOUT_MS = Number(process.env.EMBED_TIMEOUT_MS || 15000);
|
|
14
15
|
|
|
15
16
|
export function embeddingConfig() {
|
|
16
|
-
return { base: BASE, model: MODEL, enabled: Boolean(KEY) };
|
|
17
|
+
return { base: BASE, model: MODEL, enabled: Boolean(KEY) && !deterministicEnabled(), deterministic: deterministicEnabled() };
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
function postJson(urlStr, payload) {
|
|
@@ -53,7 +54,7 @@ function postJson(urlStr, payload) {
|
|
|
53
54
|
|
|
54
55
|
/** Returns array of vectors aligned to inputs, or null if unavailable. */
|
|
55
56
|
export async function embed(inputs) {
|
|
56
|
-
if (!KEY) return null;
|
|
57
|
+
if (!KEY || deterministicEnabled()) return null;
|
|
57
58
|
const arr = Array.isArray(inputs) ? inputs : [inputs];
|
|
58
59
|
if (arr.length === 0) return [];
|
|
59
60
|
const json = await postJson(BASE, { model: MODEL, input: arr });
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { deterministicEnabled } from "./env.js";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_BASE = "http://localhost:20128";
|
|
4
|
+
const DEFAULT_MODEL = "kr/claude-haiku-4.5";
|
|
5
|
+
|
|
6
|
+
function value(name) {
|
|
7
|
+
return String(process.env[name] || "").trim();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function providerEnv(prefix) {
|
|
11
|
+
const label = value(prefix);
|
|
12
|
+
const base = value(`${prefix}_BASE_URL`);
|
|
13
|
+
const key = value(`${prefix}_API_KEY`);
|
|
14
|
+
const model = value(`${prefix}_MODEL`);
|
|
15
|
+
if (!label || !base || !key || !model) return null;
|
|
16
|
+
return { label, base, key, model };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function numberedProviders() {
|
|
20
|
+
const providers = [];
|
|
21
|
+
const primary = providerEnv("MCP_PROVIDER_PRIMARY");
|
|
22
|
+
if (primary) providers.push(primary);
|
|
23
|
+
|
|
24
|
+
const slots = Object.keys(process.env)
|
|
25
|
+
.map((name) => name.match(/^MCP_PROVIDER_CHAIN(\d+)$/)?.[1])
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
.map(Number)
|
|
28
|
+
.filter((n) => n >= 2)
|
|
29
|
+
.sort((a, b) => a - b);
|
|
30
|
+
|
|
31
|
+
for (const n of slots) {
|
|
32
|
+
const provider = providerEnv(`MCP_PROVIDER_CHAIN${n}`);
|
|
33
|
+
if (provider) providers.push(provider);
|
|
34
|
+
}
|
|
35
|
+
return providers;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function legacyProvider() {
|
|
39
|
+
const base = value("MCP_RERANK_BASE_URL") || value("MCP_LLM_BASE_URL") || value("LLM_BASE_URL") || value("RERANK_URL") || value("NINEROUTER_URL") || DEFAULT_BASE;
|
|
40
|
+
const key = value("MCP_RERANK_API_KEY") || value("MCP_LLM_API_KEY") || value("LLM_API_KEY") || value("RERANK_KEY") || value("NINEROUTER_KEY");
|
|
41
|
+
const model = value("MCP_RERANK_MODEL") || value("RERANK_MODEL") || DEFAULT_MODEL;
|
|
42
|
+
if (!key) return null;
|
|
43
|
+
return { label: "legacy", base, key, model };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function providerChainConfig() {
|
|
47
|
+
const deterministic = deterministicEnabled();
|
|
48
|
+
const rerankEnabled = process.env.RERANK_ENABLED !== "0";
|
|
49
|
+
if (deterministic || !rerankEnabled) return { providers: [], enabled: false, deterministic };
|
|
50
|
+
const providers = numberedProviders();
|
|
51
|
+
if (providers.length === 0) {
|
|
52
|
+
const legacy = legacyProvider();
|
|
53
|
+
if (legacy) providers.push(legacy);
|
|
54
|
+
}
|
|
55
|
+
return { providers, enabled: providers.length > 0, deterministic };
|
|
56
|
+
}
|
package/project-memory/rerank.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LLM reranker via
|
|
2
|
+
* LLM reranker via any OpenAI-compatible /v1/chat/completions model.
|
|
3
3
|
* Used when embeddings are unavailable: keyword prefilter -> LLM picks the
|
|
4
4
|
* most relevant candidates. Degrades gracefully: returns null on any failure
|
|
5
5
|
* so callers fall back to keyword ordering. Never throws.
|
|
@@ -7,23 +7,29 @@
|
|
|
7
7
|
import http from "http";
|
|
8
8
|
import https from "https";
|
|
9
9
|
import { URL } from "url";
|
|
10
|
+
import { deterministicEnabled } from "./env.js";
|
|
11
|
+
import { providerChainConfig } from "./provider-chain.js";
|
|
10
12
|
|
|
11
|
-
const BASE = process.env.LLM_BASE_URL || process.env.RERANK_URL || process.env.NINEROUTER_URL || "http://localhost:20128";
|
|
12
|
-
const KEY = process.env.LLM_API_KEY || process.env.RERANK_KEY || process.env.NINEROUTER_KEY || "";
|
|
13
|
-
const MODEL = process.env.RERANK_MODEL || "kr/claude-haiku-4.5";
|
|
14
13
|
const TIMEOUT_MS = Number(process.env.RERANK_TIMEOUT_MS || 30000);
|
|
15
14
|
const ENABLED = process.env.RERANK_ENABLED !== "0";
|
|
16
15
|
|
|
17
16
|
export function rerankConfig() {
|
|
18
|
-
|
|
17
|
+
const cfg = providerChainConfig();
|
|
18
|
+
const first = cfg.providers[0] || {};
|
|
19
|
+
return {
|
|
20
|
+
base: first.base,
|
|
21
|
+
model: first.model,
|
|
22
|
+
providers: cfg.providers.map(({ label, base, model }) => ({ label, base, model })),
|
|
23
|
+
enabled: ENABLED && cfg.enabled && !deterministicEnabled(),
|
|
24
|
+
deterministic: deterministicEnabled(),
|
|
25
|
+
};
|
|
19
26
|
}
|
|
20
27
|
|
|
21
|
-
function
|
|
28
|
+
function chatWithProvider(provider, messages) {
|
|
22
29
|
return new Promise((resolve) => {
|
|
23
|
-
if (!KEY || !ENABLED) return resolve(null);
|
|
24
30
|
let u;
|
|
25
|
-
try { u = new URL("/v1/chat/completions",
|
|
26
|
-
const payload = Buffer.from(JSON.stringify({ model:
|
|
31
|
+
try { u = new URL("/v1/chat/completions", provider.base); } catch { return resolve(null); }
|
|
32
|
+
const payload = Buffer.from(JSON.stringify({ model: provider.model, stream: false, temperature: 0, max_tokens: 200, messages }));
|
|
27
33
|
const lib = u.protocol === "https:" ? https : http;
|
|
28
34
|
const req = lib.request(
|
|
29
35
|
{
|
|
@@ -31,7 +37,7 @@ function chat(messages) {
|
|
|
31
37
|
port: u.port || (u.protocol === "https:" ? 443 : 80),
|
|
32
38
|
path: u.pathname,
|
|
33
39
|
method: "POST",
|
|
34
|
-
headers: { "Content-Type": "application/json", "Content-Length": payload.length, Authorization: `Bearer ${
|
|
40
|
+
headers: { "Content-Type": "application/json", "Content-Length": payload.length, Authorization: `Bearer ${provider.key}` },
|
|
35
41
|
timeout: TIMEOUT_MS,
|
|
36
42
|
},
|
|
37
43
|
(res) => {
|
|
@@ -39,7 +45,6 @@ function chat(messages) {
|
|
|
39
45
|
res.on("data", (c) => (body += c));
|
|
40
46
|
res.on("end", () => {
|
|
41
47
|
if (res.statusCode !== 200) return resolve(null);
|
|
42
|
-
// handle plain JSON or SSE stream just in case
|
|
43
48
|
try {
|
|
44
49
|
const j = JSON.parse(body);
|
|
45
50
|
return resolve(j?.choices?.[0]?.message?.content ?? null);
|
|
@@ -66,12 +71,22 @@ function chat(messages) {
|
|
|
66
71
|
});
|
|
67
72
|
}
|
|
68
73
|
|
|
74
|
+
async function chat(messages) {
|
|
75
|
+
if (!ENABLED || deterministicEnabled()) return null;
|
|
76
|
+
const { providers } = providerChainConfig();
|
|
77
|
+
for (const provider of providers) {
|
|
78
|
+
const out = await chatWithProvider(provider, messages);
|
|
79
|
+
if (out) return out;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
69
84
|
/**
|
|
70
85
|
* Given a query and candidates [{id, title, snippet}], ask the model to return
|
|
71
86
|
* the ids ordered by relevance. Returns an array of ids, or null on failure.
|
|
72
87
|
*/
|
|
73
88
|
export async function rerank(query, candidates, topK = 5) {
|
|
74
|
-
if (!
|
|
89
|
+
if (!ENABLED || deterministicEnabled() || !candidates || candidates.length === 0 || !providerChainConfig().enabled) return null;
|
|
75
90
|
const list = candidates.map((c, i) => `[${i + 1}] (id:${c.id}) ${c.title}\n ${String(c.snippet || "").replace(/\s+/g, " ").slice(0, 200)}`).join("\n");
|
|
76
91
|
const sys = "You are a search reranker. Given a user query and a numbered list of notes, return the most relevant notes ordered best-first. Respond with ONLY a comma-separated list of the bracket numbers, e.g. 3,1,5. No prose.";
|
|
77
92
|
const user = `Query: ${query}\n\nNotes:\n${list}\n\nReturn the top ${topK} bracket numbers, best first, comma-separated:`;
|
package/project-memory/server.js
CHANGED
|
@@ -28,6 +28,7 @@ import os from "os";
|
|
|
28
28
|
import crypto from "crypto";
|
|
29
29
|
import { embed, embedOne, cosine, embeddingConfig } from "./embedding.js";
|
|
30
30
|
import { rerank, rerankConfig } from "./rerank.js";
|
|
31
|
+
import { deterministicEnabled } from "./env.js";
|
|
31
32
|
|
|
32
33
|
const VAULT_ROOT =
|
|
33
34
|
process.env.MEMORY_VAULT_DIR ||
|
|
@@ -318,8 +319,8 @@ class ProjectMemoryServer {
|
|
|
318
319
|
return score;
|
|
319
320
|
};
|
|
320
321
|
|
|
321
|
-
let mode = "keyword";
|
|
322
|
-
const qVec = await embedOne(query);
|
|
322
|
+
let mode = deterministicEnabled() ? "deterministic" : "keyword";
|
|
323
|
+
const qVec = deterministicEnabled() ? null : await embedOne(query);
|
|
323
324
|
const haveEmb = qVec && notes.some((n) => Array.isArray(n.embedding));
|
|
324
325
|
|
|
325
326
|
let scored;
|
|
@@ -418,6 +419,7 @@ class ProjectMemoryServer {
|
|
|
418
419
|
|
|
419
420
|
async reindex({ dir, force = false }) {
|
|
420
421
|
const p = this.paths(dir);
|
|
422
|
+
if (deterministicEnabled()) return { content: [{ type: "text", text: `Reindex ${p.slug}: skipped (deterministic no-network mode; embeddings disabled).` }] };
|
|
421
423
|
const index = await this.loadIndex(p);
|
|
422
424
|
const notes = Object.values(index.notes || {});
|
|
423
425
|
if (notes.length === 0) return { content: [{ type: "text", text: `No memories for ${p.slug} yet.` }] };
|
package/_testkit/harness.mjs
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal MCP stdio test harness — zero external deps.
|
|
3
|
-
* Provides: McpClient (spawn server, initialize, callTool), and a tiny
|
|
4
|
-
* assert/test runner (describe/it/run) with colored-ish plain output.
|
|
5
|
-
*/
|
|
6
|
-
import { spawn } from "child_process";
|
|
7
|
-
|
|
8
|
-
export class McpClient {
|
|
9
|
-
constructor(serverPath, env = {}) {
|
|
10
|
-
this.serverPath = serverPath;
|
|
11
|
-
this.env = { ...process.env, ...env };
|
|
12
|
-
this.proc = null;
|
|
13
|
-
this.buf = "";
|
|
14
|
-
this.pending = new Map();
|
|
15
|
-
this.nextId = 1;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async start() {
|
|
19
|
-
this.proc = spawn("node", [this.serverPath], { env: this.env, stdio: ["pipe", "pipe", "ignore"] });
|
|
20
|
-
this.proc.stdout.on("data", (d) => this._onData(d.toString()));
|
|
21
|
-
await this._rpc("initialize", {
|
|
22
|
-
protocolVersion: "2024-11-05",
|
|
23
|
-
capabilities: {},
|
|
24
|
-
clientInfo: { name: "testkit", version: "1.0.0" },
|
|
25
|
-
});
|
|
26
|
-
this._notify("notifications/initialized");
|
|
27
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
_onData(chunk) {
|
|
31
|
-
this.buf += chunk;
|
|
32
|
-
let idx;
|
|
33
|
-
while ((idx = this.buf.indexOf("\n")) !== -1) {
|
|
34
|
-
const line = this.buf.slice(0, idx).trim();
|
|
35
|
-
this.buf = this.buf.slice(idx + 1);
|
|
36
|
-
if (!line) continue;
|
|
37
|
-
let msg;
|
|
38
|
-
try { msg = JSON.parse(line); } catch { continue; }
|
|
39
|
-
if (msg.id != null && this.pending.has(msg.id)) {
|
|
40
|
-
const { resolve } = this.pending.get(msg.id);
|
|
41
|
-
this.pending.delete(msg.id);
|
|
42
|
-
resolve(msg);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
_send(obj) { this.proc.stdin.write(JSON.stringify(obj) + "\n"); }
|
|
48
|
-
_notify(method, params) { this._send({ jsonrpc: "2.0", method, params }); }
|
|
49
|
-
|
|
50
|
-
_rpc(method, params, timeoutMs = 30000) {
|
|
51
|
-
const id = this.nextId++;
|
|
52
|
-
return new Promise((resolve, reject) => {
|
|
53
|
-
const timer = setTimeout(() => {
|
|
54
|
-
this.pending.delete(id);
|
|
55
|
-
reject(new Error(`RPC timeout: ${method}`));
|
|
56
|
-
}, timeoutMs);
|
|
57
|
-
this.pending.set(id, { resolve: (m) => { clearTimeout(timer); resolve(m); } });
|
|
58
|
-
this._send({ jsonrpc: "2.0", id, method, params });
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async listTools() {
|
|
63
|
-
const res = await this._rpc("tools/list", {});
|
|
64
|
-
return (res.result?.tools || []).map((t) => t.name);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async callTool(name, args = {}, timeoutMs = 30000) {
|
|
68
|
-
const res = await this._rpc("tools/call", { name, arguments: args }, timeoutMs);
|
|
69
|
-
if (res.error) throw new Error(`RPC error: ${res.error.message}`);
|
|
70
|
-
const text = res.result?.content?.[0]?.text ?? "";
|
|
71
|
-
return { text, isError: Boolean(res.result?.isError), raw: res.result };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async stop() {
|
|
75
|
-
try { this.proc?.kill(); } catch { /* ignore */ }
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ---- tiny test runner ----
|
|
80
|
-
const tests = [];
|
|
81
|
-
let currentSuite = "";
|
|
82
|
-
export function describe(name, fn) { currentSuite = name; fn(); currentSuite = ""; }
|
|
83
|
-
export function it(name, fn) { tests.push({ suite: currentSuite, name, fn }); }
|
|
84
|
-
|
|
85
|
-
export function assert(cond, msg) {
|
|
86
|
-
if (!cond) throw new Error("assertion failed: " + (msg || ""));
|
|
87
|
-
}
|
|
88
|
-
export function assertIncludes(haystack, needle, msg) {
|
|
89
|
-
if (!String(haystack).includes(needle)) {
|
|
90
|
-
throw new Error(`expected to include "${needle}"${msg ? " — " + msg : ""}\n got: ${String(haystack).slice(0, 300)}`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
export function assertEqual(a, b, msg) {
|
|
94
|
-
if (a !== b) throw new Error(`expected ${JSON.stringify(b)}, got ${JSON.stringify(a)}${msg ? " — " + msg : ""}`);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export async function run() {
|
|
98
|
-
let pass = 0, fail = 0;
|
|
99
|
-
const failures = [];
|
|
100
|
-
for (const t of tests) {
|
|
101
|
-
const label = t.suite ? `${t.suite} › ${t.name}` : t.name;
|
|
102
|
-
try {
|
|
103
|
-
await t.fn();
|
|
104
|
-
pass++;
|
|
105
|
-
console.log(` ✓ ${label}`);
|
|
106
|
-
} catch (e) {
|
|
107
|
-
fail++;
|
|
108
|
-
failures.push({ label, err: e });
|
|
109
|
-
console.log(` ✗ ${label}`);
|
|
110
|
-
console.log(` ${e.message.split("\n").join("\n ")}`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
console.log(`\n${pass} passed, ${fail} failed (${tests.length} total)`);
|
|
114
|
-
if (fail > 0) process.exitCode = 1;
|
|
115
|
-
return { pass, fail, failures };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// unique temp dir helper
|
|
119
|
-
import os from "os";
|
|
120
|
-
import path from "path";
|
|
121
|
-
import fs from "fs";
|
|
122
|
-
export function tmpDir(prefix = "mcp-test-") {
|
|
123
|
-
const d = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
124
|
-
return d;
|
|
125
|
-
}
|
|
126
|
-
export function rmrf(p) { try { fs.rmSync(p, { recursive: true, force: true }); } catch { /* ignore */ } }
|
package/checkpoint/test.mjs
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { McpClient, describe, it, assert, assertIncludes, run, tmpDir, rmrf } from "../_testkit/harness.mjs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import fs from "fs";
|
|
4
|
-
import { fileURLToPath } from "url";
|
|
5
|
-
|
|
6
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const SERVER = path.join(__dirname, "server.js");
|
|
8
|
-
const STORE = tmpDir("cp-store-");
|
|
9
|
-
const PROJ = tmpDir("cp-proj-");
|
|
10
|
-
|
|
11
|
-
// seed project files
|
|
12
|
-
fs.mkdirSync(path.join(PROJ, "src"), { recursive: true });
|
|
13
|
-
fs.writeFileSync(path.join(PROJ, "src", "a.txt"), "original A\n");
|
|
14
|
-
fs.writeFileSync(path.join(PROJ, "README.md"), "# Title\n");
|
|
15
|
-
|
|
16
|
-
const client = new McpClient(SERVER, { CHECKPOINT_DIR: STORE });
|
|
17
|
-
let cpId = "";
|
|
18
|
-
|
|
19
|
-
describe("checkpoint", () => {
|
|
20
|
-
it("lists expected tools", async () => {
|
|
21
|
-
const tools = await client.listTools();
|
|
22
|
-
for (const t of ["checkpoint_create", "checkpoint_list", "checkpoint_restore", "checkpoint_diff", "checkpoint_delete"])
|
|
23
|
-
assert(tools.includes(t), `missing ${t}`);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("creates a checkpoint", async () => {
|
|
27
|
-
const r = await client.callTool("checkpoint_create", { dir: PROJ, label: "baseline" });
|
|
28
|
-
assertIncludes(r.text, "Checkpoint");
|
|
29
|
-
assertIncludes(r.text, "Files stored: 2");
|
|
30
|
-
cpId = (r.text.match(/Checkpoint (\S+) created/) || [])[1] || "";
|
|
31
|
-
assert(cpId, "could not parse checkpoint id");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("diff detects add/modify/delete", async () => {
|
|
35
|
-
fs.writeFileSync(path.join(PROJ, "src", "a.txt"), "CHANGED A\n");
|
|
36
|
-
fs.writeFileSync(path.join(PROJ, "src", "new.txt"), "brand new\n");
|
|
37
|
-
fs.rmSync(path.join(PROJ, "README.md"));
|
|
38
|
-
const r = await client.callTool("checkpoint_diff", { dir: PROJ, id: cpId });
|
|
39
|
-
assertIncludes(r.text, "Added (1)");
|
|
40
|
-
assertIncludes(r.text, "src/new.txt");
|
|
41
|
-
assertIncludes(r.text, "Modified (1)");
|
|
42
|
-
assertIncludes(r.text, "src/a.txt");
|
|
43
|
-
assertIncludes(r.text, "Deleted (1)");
|
|
44
|
-
assertIncludes(r.text, "README.md");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("restores with clean (revert + remove new)", async () => {
|
|
48
|
-
const r = await client.callTool("checkpoint_restore", { dir: PROJ, id: cpId, clean: true });
|
|
49
|
-
assertIncludes(r.text, "Restored 2 files");
|
|
50
|
-
assert(fs.readFileSync(path.join(PROJ, "src", "a.txt"), "utf8").includes("original A"), "a.txt not reverted");
|
|
51
|
-
assert(fs.existsSync(path.join(PROJ, "README.md")), "README not restored");
|
|
52
|
-
assert(!fs.existsSync(path.join(PROJ, "src", "new.txt")), "new.txt not removed");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("errors on unknown id", async () => {
|
|
56
|
-
const r = await client.callTool("checkpoint_diff", { dir: PROJ, id: "nope-123" });
|
|
57
|
-
assert(r.isError, "expected error flag");
|
|
58
|
-
assertIncludes(r.text, "not found");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("deletes the checkpoint", async () => {
|
|
62
|
-
const r = await client.callTool("checkpoint_delete", { dir: PROJ, id: cpId });
|
|
63
|
-
assertIncludes(r.text, "Deleted checkpoint");
|
|
64
|
-
const r2 = await client.callTool("checkpoint_list", { dir: PROJ });
|
|
65
|
-
assertIncludes(r2.text, "No checkpoints");
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
await client.start();
|
|
70
|
-
const { fail } = await run();
|
|
71
|
-
await client.stop();
|
|
72
|
-
rmrf(STORE); rmrf(PROJ);
|
|
73
|
-
process.exit(fail > 0 ? 1 : 0);
|
package/context-pack/test.mjs
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { McpClient, describe, it, assert, assertIncludes, run, tmpDir, rmrf } from "../_testkit/harness.mjs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import fs from "fs";
|
|
4
|
-
import { fileURLToPath } from "url";
|
|
5
|
-
|
|
6
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const SERVER = path.join(__dirname, "server.js");
|
|
8
|
-
const PROJ = tmpDir("ctx-proj-");
|
|
9
|
-
|
|
10
|
-
fs.mkdirSync(path.join(PROJ, "src"), { recursive: true });
|
|
11
|
-
fs.writeFileSync(path.join(PROJ, "package.json"), JSON.stringify({
|
|
12
|
-
name: "sample", version: "1.0.0",
|
|
13
|
-
scripts: { dev: "vite", test: "vitest" },
|
|
14
|
-
dependencies: { react: "^18.0.0", next: "^14.0.0" },
|
|
15
|
-
}, null, 2));
|
|
16
|
-
fs.writeFileSync(path.join(PROJ, "README.md"), "# Sample Project\nDemo app.\n## Setup\nnpm install\n");
|
|
17
|
-
fs.writeFileSync(path.join(PROJ, "src", "auth.ts"),
|
|
18
|
-
"export function login(user: string) { return true; }\nexport class AuthService { verify() {} }\nfunction helperInternal() {}\n");
|
|
19
|
-
|
|
20
|
-
const client = new McpClient(SERVER, {});
|
|
21
|
-
|
|
22
|
-
describe("context-pack", () => {
|
|
23
|
-
it("lists expected tools", async () => {
|
|
24
|
-
const tools = await client.listTools();
|
|
25
|
-
for (const t of ["pack_overview", "pack_tree", "pack_outline", "pack_search"])
|
|
26
|
-
assert(tools.includes(t), `missing ${t}`);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("overview detects stack + scripts + readme", async () => {
|
|
30
|
-
const r = await client.callTool("pack_overview", { dir: PROJ });
|
|
31
|
-
assertIncludes(r.text, "Next.js");
|
|
32
|
-
assertIncludes(r.text, "React");
|
|
33
|
-
assertIncludes(r.text, "dev: vite");
|
|
34
|
-
assertIncludes(r.text, "Sample Project");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("tree shows dirs and files", async () => {
|
|
38
|
-
const r = await client.callTool("pack_tree", { dir: PROJ, max_depth: 3 });
|
|
39
|
-
assertIncludes(r.text, "src/");
|
|
40
|
-
assertIncludes(r.text, "auth.ts");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("outline extracts symbols", async () => {
|
|
44
|
-
const r = await client.callTool("pack_outline", { dir: PROJ, file: "src/auth.ts" });
|
|
45
|
-
assertIncludes(r.text, "login");
|
|
46
|
-
assertIncludes(r.text, "AuthService");
|
|
47
|
-
assertIncludes(r.text, "helperInternal");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("outline errors on missing file", async () => {
|
|
51
|
-
const r = await client.callTool("pack_outline", { dir: PROJ, file: "does/not/exist.ts" });
|
|
52
|
-
assert(r.isError, "expected error");
|
|
53
|
-
assertIncludes(r.text, "File not found");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("search matches by path substring", async () => {
|
|
57
|
-
const r = await client.callTool("pack_search", { dir: PROJ, query: "auth" });
|
|
58
|
-
assertIncludes(r.text, "src/auth.ts");
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
await client.start();
|
|
63
|
-
const { fail } = await run();
|
|
64
|
-
await client.stop();
|
|
65
|
-
rmrf(PROJ);
|
|
66
|
-
process.exit(fail > 0 ? 1 : 0);
|
package/devjournal/test.mjs
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { McpClient, describe, it, assert, assertIncludes, run, tmpDir, rmrf } from "../_testkit/harness.mjs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { fileURLToPath } from "url";
|
|
4
|
-
|
|
5
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
const SERVER = path.join(__dirname, "server.js");
|
|
7
|
-
const STORE = tmpDir("jrnl-");
|
|
8
|
-
const PROJ = "/tmp/jrnl-demo-project";
|
|
9
|
-
|
|
10
|
-
const client = new McpClient(SERVER, { JOURNAL_DIR: STORE });
|
|
11
|
-
|
|
12
|
-
describe("devjournal", () => {
|
|
13
|
-
it("lists expected tools", async () => {
|
|
14
|
-
const tools = await client.listTools();
|
|
15
|
-
for (const t of ["journal_log", "journal_handoff", "journal_resume", "journal_timeline", "journal_search", "journal_clear_handoff"])
|
|
16
|
-
assert(tools.includes(t), `missing ${t}`);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("logs entries", async () => {
|
|
20
|
-
const r1 = await client.callTool("journal_log", { dir: PROJ, title: "Set up auth", type: "done", body: "login + AuthService" });
|
|
21
|
-
assertIncludes(r1.text, "Logged [done]");
|
|
22
|
-
const r2 = await client.callTool("journal_log", { dir: PROJ, title: "Refresh undecided", type: "blocker" });
|
|
23
|
-
assertIncludes(r2.text, "Logged [blocker]");
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("saves a handoff", async () => {
|
|
27
|
-
const r = await client.callTool("journal_handoff", {
|
|
28
|
-
dir: PROJ, summary: "Auth scaffolding done; wiring refresh next.",
|
|
29
|
-
next_steps: ["Implement refresh", "Add tests"],
|
|
30
|
-
open_questions: ["Cookie or header?"], active_files: ["src/auth.ts"],
|
|
31
|
-
});
|
|
32
|
-
assertIncludes(r.text, "Handoff saved");
|
|
33
|
-
assertIncludes(r.text, "Next steps: 2");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("resume returns handoff + recent entries", async () => {
|
|
37
|
-
const r = await client.callTool("journal_resume", { dir: PROJ });
|
|
38
|
-
assertIncludes(r.text, "Latest handoff");
|
|
39
|
-
assertIncludes(r.text, "Implement refresh");
|
|
40
|
-
assertIncludes(r.text, "Cookie or header?");
|
|
41
|
-
assertIncludes(r.text, "Recent entries");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("timeline filters by type", async () => {
|
|
45
|
-
const r = await client.callTool("journal_timeline", { dir: PROJ, type: "blocker" });
|
|
46
|
-
assertIncludes(r.text, "Refresh undecided");
|
|
47
|
-
assert(!r.text.includes("Set up auth"), "should not include done entries when filtering blocker");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("search finds entries by keyword (offline mode)", async () => {
|
|
51
|
-
const r = await client.callTool("journal_search", { dir: PROJ, query: "auth login refresh" });
|
|
52
|
-
assertIncludes(r.text, "[keyword]");
|
|
53
|
-
assertIncludes(r.text, "Set up auth");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("clears handoff", async () => {
|
|
57
|
-
const r = await client.callTool("journal_clear_handoff", { dir: PROJ });
|
|
58
|
-
assertIncludes(r.text, "Cleared handoff");
|
|
59
|
-
const r2 = await client.callTool("journal_resume", { dir: PROJ });
|
|
60
|
-
assert(!r2.text.includes("Latest handoff"), "handoff should be gone");
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
await client.start();
|
|
65
|
-
const { fail } = await run();
|
|
66
|
-
await client.stop();
|
|
67
|
-
rmrf(STORE);
|
|
68
|
-
process.exit(fail > 0 ? 1 : 0);
|
package/project-memory/test.mjs
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { McpClient, describe, it, assert, assertIncludes, run, tmpDir, rmrf } from "../_testkit/harness.mjs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { fileURLToPath } from "url";
|
|
4
|
-
|
|
5
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
const SERVER = path.join(__dirname, "server.js");
|
|
7
|
-
const VAULT = tmpDir("pm-");
|
|
8
|
-
const DEMO = "/tmp/pm-demo-project";
|
|
9
|
-
|
|
10
|
-
// Force keyword mode (no embedding key) so the suite is deterministic/offline.
|
|
11
|
-
const client = new McpClient(SERVER, { MEMORY_VAULT_DIR: VAULT, NINEROUTER_KEY: "", EMBED_KEY: "" });
|
|
12
|
-
|
|
13
|
-
let savedId = "";
|
|
14
|
-
|
|
15
|
-
describe("project-memory", () => {
|
|
16
|
-
it("lists expected tools", async () => {
|
|
17
|
-
const tools = await client.listTools();
|
|
18
|
-
for (const t of ["memory_save", "memory_recall", "memory_list", "memory_get", "memory_delete", "memory_reindex"])
|
|
19
|
-
assert(tools.includes(t), `missing ${t}`);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("saves a note (keyword mode without embeddings)", async () => {
|
|
23
|
-
const r = await client.callTool("memory_save", {
|
|
24
|
-
dir: DEMO, title: "JWT login flow",
|
|
25
|
-
content: "login() issues JWT; refresh token in httpOnly cookie; clock skew bug on verify.",
|
|
26
|
-
tags: ["auth", "jwt"], kind: "decision",
|
|
27
|
-
});
|
|
28
|
-
assertIncludes(r.text, "Saved note");
|
|
29
|
-
assertIncludes(r.text, "keyword-only");
|
|
30
|
-
savedId = (r.text.match(/Saved note (\S+)/) || [])[1] || "";
|
|
31
|
-
assert(savedId, "could not parse note id");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("recalls by keyword and tags mode label", async () => {
|
|
35
|
-
const r = await client.callTool("memory_recall", { dir: DEMO, query: "jwt auth cookie" });
|
|
36
|
-
assertIncludes(r.text, "[keyword]");
|
|
37
|
-
assertIncludes(r.text, "JWT login flow");
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("returns no-match gracefully", async () => {
|
|
41
|
-
const r = await client.callTool("memory_recall", { dir: DEMO, query: "kubernetes helm chart xyzzy" });
|
|
42
|
-
assertIncludes(r.text, "No relevant memories");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("lists saved notes", async () => {
|
|
46
|
-
const r = await client.callTool("memory_list", { dir: DEMO });
|
|
47
|
-
assertIncludes(r.text, "JWT login flow");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("gets full note by id", async () => {
|
|
51
|
-
const r = await client.callTool("memory_get", { dir: DEMO, id: savedId });
|
|
52
|
-
assertIncludes(r.text, "httpOnly cookie");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("reindex reports gracefully without embeddings", async () => {
|
|
56
|
-
const r = await client.callTool("memory_reindex", { dir: DEMO });
|
|
57
|
-
assertIncludes(r.text, "failed");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("deletes a note", async () => {
|
|
61
|
-
const r = await client.callTool("memory_delete", { dir: DEMO, id: savedId });
|
|
62
|
-
assertIncludes(r.text, "Deleted note");
|
|
63
|
-
const r2 = await client.callTool("memory_list", { dir: DEMO });
|
|
64
|
-
assertIncludes(r2.text, "No memories");
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
await client.start();
|
|
69
|
-
const { fail } = await run();
|
|
70
|
-
await client.stop();
|
|
71
|
-
rmrf(VAULT);
|
|
72
|
-
process.exit(fail > 0 ? 1 : 0);
|