continuous-improvement 2.2.0 → 3.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.
@@ -0,0 +1,663 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * continuous-improvement MCP Server
5
+ *
6
+ * Exposes instincts, observations, and reflection as MCP tools + resources.
7
+ * Two modes: beginner (3 tools) and expert (all tools).
8
+ *
9
+ * Usage:
10
+ * node bin/mcp-server.mjs # default: beginner mode
11
+ * node bin/mcp-server.mjs --mode expert # all tools
12
+ * node bin/mcp-server.mjs --mode beginner # explicit beginner
13
+ */
14
+
15
+ import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync } from "node:fs";
16
+ import { join, basename, dirname } from "node:path";
17
+ import { homedir } from "node:os";
18
+ import { fileURLToPath } from "node:url";
19
+ import { execSync } from "node:child_process";
20
+ import { createInterface } from "node:readline";
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Config
24
+ // ---------------------------------------------------------------------------
25
+ const VERSION = "3.1.0";
26
+ const INSTINCTS_DIR = join(homedir(), ".claude", "instincts");
27
+ const GLOBAL_DIR = join(INSTINCTS_DIR, "global");
28
+
29
+ const args = process.argv.slice(2);
30
+ const modeIdx = args.indexOf("--mode");
31
+ const MODE = modeIdx !== -1 && args[modeIdx + 1] ? args[modeIdx + 1] : "beginner";
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Helpers
35
+ // ---------------------------------------------------------------------------
36
+
37
+ function getProjectHash() {
38
+ try {
39
+ const root = execSync("git rev-parse --show-toplevel 2>/dev/null", { encoding: "utf8" }).trim();
40
+ const hash = execSync(`printf '%s' "${root}" | sha256sum | cut -c1-12`, {
41
+ encoding: "utf8",
42
+ shell: "/bin/bash",
43
+ }).trim();
44
+ return { root, hash, name: basename(root) };
45
+ } catch {
46
+ return { root: "global", hash: "global", name: "global" };
47
+ }
48
+ }
49
+
50
+ function readInstincts(projectHash) {
51
+ const instincts = [];
52
+ const dirs = [GLOBAL_DIR];
53
+ if (projectHash !== "global") {
54
+ dirs.push(join(INSTINCTS_DIR, projectHash));
55
+ }
56
+
57
+ for (const dir of dirs) {
58
+ if (!existsSync(dir)) continue;
59
+ for (const file of readdirSync(dir)) {
60
+ if (!file.endsWith(".yaml")) continue;
61
+ try {
62
+ const content = readFileSync(join(dir, file), "utf8");
63
+ const parsed = parseYamlInstinct(content);
64
+ if (parsed) instincts.push(parsed);
65
+ } catch {
66
+ // skip malformed files
67
+ }
68
+ }
69
+ }
70
+ return instincts;
71
+ }
72
+
73
+ function parseYamlInstinct(content) {
74
+ const lines = content.split("\n");
75
+ const meta = {};
76
+ let body = "";
77
+ let inBody = false;
78
+
79
+ for (const line of lines) {
80
+ if (line.trim() === "---" && Object.keys(meta).length > 0) {
81
+ inBody = true;
82
+ continue;
83
+ }
84
+ if (inBody) {
85
+ body += line + "\n";
86
+ } else {
87
+ const match = line.match(/^(\w[\w_-]*):\s*(.+)/);
88
+ if (match) {
89
+ let val = match[2].trim().replace(/^["']|["']$/g, "");
90
+ if (!isNaN(val) && val !== "") val = parseFloat(val);
91
+ meta[match[1]] = val;
92
+ }
93
+ }
94
+ }
95
+
96
+ if (!meta.id) return null;
97
+ return { ...meta, body: body.trim() };
98
+ }
99
+
100
+ function countObservations(projectHash) {
101
+ const obsFile = join(INSTINCTS_DIR, projectHash, "observations.jsonl");
102
+ if (!existsSync(obsFile)) return 0;
103
+ try {
104
+ const content = readFileSync(obsFile, "utf8");
105
+ return content.split("\n").filter((l) => l.trim()).length;
106
+ } catch {
107
+ return 0;
108
+ }
109
+ }
110
+
111
+ function getRecentObservations(projectHash, limit = 50) {
112
+ const obsFile = join(INSTINCTS_DIR, projectHash, "observations.jsonl");
113
+ if (!existsSync(obsFile)) return [];
114
+ try {
115
+ const lines = readFileSync(obsFile, "utf8").split("\n").filter((l) => l.trim());
116
+ return lines.slice(-limit).map((l) => {
117
+ try { return JSON.parse(l); } catch { return null; }
118
+ }).filter(Boolean);
119
+ } catch {
120
+ return [];
121
+ }
122
+ }
123
+
124
+ function detectLevel(projectHash) {
125
+ const obsCount = countObservations(projectHash);
126
+ const instincts = readInstincts(projectHash);
127
+ const hasHighConfidence = instincts.some((i) => i.confidence >= 0.7);
128
+ const hasMidConfidence = instincts.some((i) => i.confidence >= 0.5 && i.confidence < 0.7);
129
+
130
+ if (hasHighConfidence) return "AUTO-APPLY";
131
+ if (hasMidConfidence) return "SUGGEST";
132
+ if (obsCount >= 20 || instincts.length > 0) return "ANALYZE";
133
+ return "CAPTURE";
134
+ }
135
+
136
+ function writeInstinct(projectHash, instinct) {
137
+ const dir = instinct.scope === "global" ? GLOBAL_DIR : join(INSTINCTS_DIR, projectHash);
138
+ mkdirSync(dir, { recursive: true });
139
+
140
+ const yaml = [
141
+ `id: ${instinct.id}`,
142
+ `trigger: "${instinct.trigger}"`,
143
+ `confidence: ${instinct.confidence}`,
144
+ `domain: ${instinct.domain || "workflow"}`,
145
+ `source: ${instinct.source || "manual"}`,
146
+ `scope: ${instinct.scope || "project"}`,
147
+ `project_id: ${projectHash}`,
148
+ `created: "${new Date().toISOString().split("T")[0]}"`,
149
+ `last_seen: "${new Date().toISOString().split("T")[0]}"`,
150
+ `observation_count: ${instinct.observation_count || 1}`,
151
+ "---",
152
+ instinct.body,
153
+ ].join("\n");
154
+
155
+ writeFileSync(join(dir, `${instinct.id}.yaml`), yaml + "\n");
156
+ }
157
+
158
+ function updateInstinctConfidence(projectHash, instinctId, delta) {
159
+ const instincts = readInstincts(projectHash);
160
+ const instinct = instincts.find((i) => i.id === instinctId);
161
+ if (!instinct) return null;
162
+
163
+ const newConf = Math.max(0, Math.min(0.9, (instinct.confidence || 0.5) + delta));
164
+ instinct.confidence = Math.round(newConf * 100) / 100;
165
+ instinct.last_seen = new Date().toISOString().split("T")[0];
166
+ writeInstinct(projectHash, instinct);
167
+ return instinct;
168
+ }
169
+
170
+ // ---------------------------------------------------------------------------
171
+ // MCP Protocol (JSON-RPC over stdio, no SDK dependency)
172
+ // ---------------------------------------------------------------------------
173
+
174
+ const BEGINNER_TOOLS = [
175
+ {
176
+ name: "ci_status",
177
+ description: "Show current level, instinct count, and observation count for this project. Good starting point to see what the system has learned.",
178
+ inputSchema: { type: "object", properties: {}, required: [] },
179
+ },
180
+ {
181
+ name: "ci_instincts",
182
+ description: "List all learned instincts for this project with their confidence levels and behaviors.",
183
+ inputSchema: {
184
+ type: "object",
185
+ properties: {
186
+ min_confidence: { type: "number", description: "Minimum confidence to show (default: 0)", default: 0 },
187
+ },
188
+ required: [],
189
+ },
190
+ },
191
+ {
192
+ name: "ci_reflect",
193
+ description: "Generate a structured reflection for the current session. Provide a summary of what you worked on.",
194
+ inputSchema: {
195
+ type: "object",
196
+ properties: {
197
+ summary: { type: "string", description: "Brief summary of what was done this session" },
198
+ },
199
+ required: ["summary"],
200
+ },
201
+ },
202
+ ];
203
+
204
+ const PACKS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "instinct-packs");
205
+
206
+ const EXPERT_TOOLS = [
207
+ {
208
+ name: "ci_reinforce",
209
+ description: "Accept or reject an instinct suggestion. Adjusts confidence: +0.15 for accept, -0.1 for reject.",
210
+ inputSchema: {
211
+ type: "object",
212
+ properties: {
213
+ instinct_id: { type: "string", description: "The instinct ID to reinforce" },
214
+ accepted: { type: "boolean", description: "true = accept (+0.15), false = reject (-0.1)" },
215
+ },
216
+ required: ["instinct_id", "accepted"],
217
+ },
218
+ },
219
+ {
220
+ name: "ci_create_instinct",
221
+ description: "Manually create a new instinct with a trigger, body, and starting confidence.",
222
+ inputSchema: {
223
+ type: "object",
224
+ properties: {
225
+ id: { type: "string", description: "Unique instinct ID (kebab-case)" },
226
+ trigger: { type: "string", description: "When this instinct applies" },
227
+ body: { type: "string", description: "The behavior to follow" },
228
+ confidence: { type: "number", description: "Starting confidence 0.0-0.9 (default: 0.6)", default: 0.6 },
229
+ domain: { type: "string", description: "Domain: workflow|tooling|testing|patterns|code-style", default: "workflow" },
230
+ scope: { type: "string", description: "Scope: project|global", default: "project" },
231
+ },
232
+ required: ["id", "trigger", "body"],
233
+ },
234
+ },
235
+ {
236
+ name: "ci_observations",
237
+ description: "View recent tool call observations captured by hooks.",
238
+ inputSchema: {
239
+ type: "object",
240
+ properties: {
241
+ limit: { type: "number", description: "Number of recent observations to return (default: 20)", default: 20 },
242
+ },
243
+ required: [],
244
+ },
245
+ },
246
+ {
247
+ name: "ci_export",
248
+ description: "Export all instincts as a JSON array for sharing or backup.",
249
+ inputSchema: {
250
+ type: "object",
251
+ properties: {
252
+ scope: { type: "string", description: "Which instincts: project|global|all (default: all)", default: "all" },
253
+ },
254
+ required: [],
255
+ },
256
+ },
257
+ {
258
+ name: "ci_import",
259
+ description: "Import instincts from a JSON array. Skips duplicates by ID.",
260
+ inputSchema: {
261
+ type: "object",
262
+ properties: {
263
+ instincts_json: { type: "string", description: "JSON array of instinct objects to import" },
264
+ scope: { type: "string", description: "Import to: project|global (default: project)", default: "project" },
265
+ },
266
+ required: ["instincts_json"],
267
+ },
268
+ },
269
+ {
270
+ name: "ci_dashboard",
271
+ description: "Visual dashboard showing instinct health, observation stats, confidence distribution, and learning progress.",
272
+ inputSchema: { type: "object", properties: {}, required: [] },
273
+ },
274
+ {
275
+ name: "ci_load_pack",
276
+ description: "Load a starter instinct pack (react, python, go) into the current project.",
277
+ inputSchema: {
278
+ type: "object",
279
+ properties: {
280
+ pack: { type: "string", description: "Pack name: react, python, or go" },
281
+ },
282
+ required: ["pack"],
283
+ },
284
+ },
285
+ ];
286
+
287
+ function getAllTools() {
288
+ if (MODE === "expert") return [...BEGINNER_TOOLS, ...EXPERT_TOOLS];
289
+ return BEGINNER_TOOLS;
290
+ }
291
+
292
+ // ---------------------------------------------------------------------------
293
+ // Tool handlers
294
+ // ---------------------------------------------------------------------------
295
+
296
+ function handleTool(name, params) {
297
+ const project = getProjectHash();
298
+
299
+ switch (name) {
300
+ case "ci_status": {
301
+ const level = detectLevel(project.hash);
302
+ const obsCount = countObservations(project.hash);
303
+ const instincts = readInstincts(project.hash);
304
+ const byConfidence = {
305
+ silent: instincts.filter((i) => i.confidence < 0.5).length,
306
+ suggest: instincts.filter((i) => i.confidence >= 0.5 && i.confidence < 0.7).length,
307
+ autoApply: instincts.filter((i) => i.confidence >= 0.7).length,
308
+ };
309
+
310
+ return text([
311
+ `## continuous-improvement Status`,
312
+ ``,
313
+ `**Project:** ${project.name}`,
314
+ `**Level:** ${level}`,
315
+ `**Observations:** ${obsCount}`,
316
+ `**Instincts:** ${instincts.length} total`,
317
+ ` - Silent (< 0.5): ${byConfidence.silent}`,
318
+ ` - Suggest (0.5-0.69): ${byConfidence.suggest}`,
319
+ ` - Auto-apply (0.7+): ${byConfidence.autoApply}`,
320
+ ``,
321
+ `**Mode:** ${MODE}`,
322
+ level === "CAPTURE" ? `\n_Keep working — hooks are capturing. Analysis begins at 20 observations._` : "",
323
+ ].join("\n"));
324
+ }
325
+
326
+ case "ci_instincts": {
327
+ const minConf = params.min_confidence || 0;
328
+ const instincts = readInstincts(project.hash).filter((i) => i.confidence >= minConf);
329
+
330
+ if (instincts.length === 0) return text("No instincts found. Keep working — the system learns from your sessions.");
331
+
332
+ const lines = instincts
333
+ .sort((a, b) => b.confidence - a.confidence)
334
+ .map((i) => {
335
+ const behavior = i.confidence >= 0.7 ? "AUTO-APPLY" : i.confidence >= 0.5 ? "SUGGEST" : "silent";
336
+ return `- **${i.id}** (${i.confidence}) [${behavior}]\n Trigger: ${i.trigger}\n ${i.body}`;
337
+ });
338
+
339
+ return text(`## Instincts for ${project.name}\n\n${lines.join("\n\n")}`);
340
+ }
341
+
342
+ case "ci_reflect": {
343
+ const summary = params.summary || "No summary provided";
344
+ const reflection = [
345
+ `## Reflection — ${new Date().toISOString().split("T")[0]}`,
346
+ ``,
347
+ `**Session summary:** ${summary}`,
348
+ ``,
349
+ `Use this template to reflect:`,
350
+ `- **What worked:**`,
351
+ `- **What failed:**`,
352
+ `- **What I'd do differently:**`,
353
+ `- **Rule to add:** (becomes an instinct at 0.6 confidence)`,
354
+ ].join("\n");
355
+
356
+ return text(reflection);
357
+ }
358
+
359
+ case "ci_reinforce": {
360
+ if (MODE !== "expert") return error("ci_reinforce requires expert mode. Start server with --mode expert");
361
+ const delta = params.accepted ? 0.15 : -0.1;
362
+ const updated = updateInstinctConfidence(project.hash, params.instinct_id, delta);
363
+ if (!updated) return error(`Instinct "${params.instinct_id}" not found`);
364
+ return text(`${params.accepted ? "Accepted" : "Rejected"} **${updated.id}** — confidence now ${updated.confidence}`);
365
+ }
366
+
367
+ case "ci_create_instinct": {
368
+ if (MODE !== "expert") return error("ci_create_instinct requires expert mode");
369
+ writeInstinct(project.hash, {
370
+ id: params.id,
371
+ trigger: params.trigger,
372
+ body: params.body,
373
+ confidence: params.confidence || 0.6,
374
+ domain: params.domain || "workflow",
375
+ source: "manual",
376
+ scope: params.scope || "project",
377
+ observation_count: 1,
378
+ });
379
+ return text(`Created instinct **${params.id}** with confidence ${params.confidence || 0.6}`);
380
+ }
381
+
382
+ case "ci_observations": {
383
+ if (MODE !== "expert") return error("ci_observations requires expert mode");
384
+ const limit = params.limit || 20;
385
+ const obs = getRecentObservations(project.hash, limit);
386
+ if (obs.length === 0) return text("No observations yet. Hooks capture tool calls automatically.");
387
+ const lines = obs.map((o) => `[${o.ts}] ${o.event} — ${o.tool}`);
388
+ return text(`## Recent Observations (${obs.length})\n\n${lines.join("\n")}`);
389
+ }
390
+
391
+ case "ci_export": {
392
+ if (MODE !== "expert") return error("ci_export requires expert mode");
393
+ const scope = params.scope || "all";
394
+ let instincts = readInstincts(project.hash);
395
+ if (scope === "project") instincts = instincts.filter((i) => i.scope === "project");
396
+ if (scope === "global") instincts = instincts.filter((i) => i.scope === "global");
397
+ return text(JSON.stringify(instincts, null, 2));
398
+ }
399
+
400
+ case "ci_import": {
401
+ if (MODE !== "expert") return error("ci_import requires expert mode");
402
+ let toImport;
403
+ try {
404
+ toImport = JSON.parse(params.instincts_json);
405
+ } catch {
406
+ return error("Invalid JSON. Provide a JSON array of instinct objects.");
407
+ }
408
+ if (!Array.isArray(toImport)) return error("Expected a JSON array");
409
+
410
+ const existing = readInstincts(project.hash);
411
+ const existingIds = new Set(existing.map((i) => i.id));
412
+ let imported = 0;
413
+
414
+ for (const inst of toImport) {
415
+ if (!inst.id || !inst.trigger || !inst.body) continue;
416
+ if (existingIds.has(inst.id)) continue;
417
+ writeInstinct(project.hash, {
418
+ ...inst,
419
+ scope: params.scope || inst.scope || "project",
420
+ source: "imported",
421
+ });
422
+ imported++;
423
+ }
424
+
425
+ return text(`Imported ${imported} instincts (${toImport.length - imported} skipped as duplicates)`);
426
+ }
427
+
428
+ case "ci_dashboard": {
429
+ if (MODE !== "expert") return error("ci_dashboard requires expert mode");
430
+ const level = detectLevel(project.hash);
431
+ const obsCount = countObservations(project.hash);
432
+ const instincts = readInstincts(project.hash);
433
+ const byConf = {
434
+ auto: instincts.filter((i) => i.confidence >= 0.7),
435
+ suggest: instincts.filter((i) => i.confidence >= 0.5 && i.confidence < 0.7),
436
+ silent: instincts.filter((i) => i.confidence < 0.5),
437
+ };
438
+ const globalCount = instincts.filter((i) => i.scope === "global").length;
439
+ const projectCount = instincts.length - globalCount;
440
+ const today = new Date();
441
+ const stale = instincts.filter((i) => {
442
+ if (!i.last_seen) return false;
443
+ const diff = (today - new Date(i.last_seen)) / (1000 * 60 * 60 * 24);
444
+ return diff > 30;
445
+ });
446
+
447
+ const autoBar = "█".repeat(Math.min(10, byConf.auto.length)) + "░".repeat(Math.max(0, 10 - byConf.auto.length));
448
+ const sugBar = "█".repeat(Math.min(10, byConf.suggest.length)) + "░".repeat(Math.max(0, 10 - byConf.suggest.length));
449
+ const silBar = "█".repeat(Math.min(10, byConf.silent.length)) + "░".repeat(Math.max(0, 10 - byConf.silent.length));
450
+
451
+ const top5 = instincts
452
+ .sort((a, b) => b.confidence - a.confidence)
453
+ .slice(0, 5)
454
+ .map((i) => ` ${("█".repeat(Math.round(i.confidence * 10)) + "░".repeat(10 - Math.round(i.confidence * 10)))} ${i.confidence.toFixed(2)} ${i.id}`)
455
+ .join("\n");
456
+
457
+ // Check available packs
458
+ let packInfo = "";
459
+ if (existsSync(PACKS_DIR)) {
460
+ const packs = readdirSync(PACKS_DIR).filter((f) => f.endsWith(".json")).map((f) => f.replace(".json", ""));
461
+ packInfo = `\n Available packs: ${packs.join(", ")}`;
462
+ }
463
+
464
+ return text([
465
+ `╔══════════════════════════════════════════════════════════════╗`,
466
+ `║ continuous-improvement Dashboard ║`,
467
+ `╠══════════════════════════════════════════════════════════════╣`,
468
+ `║ ║`,
469
+ `║ Project: ${project.name.padEnd(20)} Level: ${level.padEnd(12)} ║`,
470
+ `║ Sessions: ~${String(Math.floor(obsCount / 10)).padEnd(17)} Mode: ${MODE.padEnd(13)} ║`,
471
+ `║ ║`,
472
+ `║ ┌─ Observations ────────────────────────────────────────┐ ║`,
473
+ `║ │ Total: ${String(obsCount).padEnd(48)} │ ║`,
474
+ `║ └───────────────────────────────────────────────────────┘ ║`,
475
+ `║ ║`,
476
+ `║ ┌─ Instincts ───────────────────────────────────────────┐ ║`,
477
+ `║ │ Total: ${String(instincts.length).padEnd(48)} │ ║`,
478
+ `║ │ ${autoBar} Auto-apply (0.7+): ${String(byConf.auto.length).padEnd(20)} │ ║`,
479
+ `║ │ ${sugBar} Suggest (0.5-0.69): ${String(byConf.suggest.length).padEnd(19)} │ ║`,
480
+ `║ │ ${silBar} Silent (< 0.5): ${String(byConf.silent.length).padEnd(23)} │ ║`,
481
+ `║ │ Global: ${String(globalCount).padEnd(10)} Project: ${String(projectCount).padEnd(28)} │ ║`,
482
+ `║ └───────────────────────────────────────────────────────┘ ║`,
483
+ `║ ║`,
484
+ instincts.length > 0 ? [
485
+ `║ ┌─ Top Instincts ───────────────────────────────────────┐ ║`,
486
+ ...top5.split("\n").map((l) => `║ │${l.padEnd(56)}│ ║`),
487
+ `║ └───────────────────────────────────────────────────────┘ ║`,
488
+ ].join("\n") : "",
489
+ `║ ║`,
490
+ `║ ┌─ Health ──────────────────────────────────────────────┐ ║`,
491
+ `║ │ Stale (30+ days): ${String(stale.length).padEnd(38)} │ ║`,
492
+ `║ └───────────────────────────────────────────────────────┘ ║`,
493
+ packInfo ? `║${packInfo.padEnd(63)}║` : "",
494
+ `║ ║`,
495
+ `╚══════════════════════════════════════════════════════════════╝`,
496
+ ].filter(Boolean).join("\n"));
497
+ }
498
+
499
+ case "ci_load_pack": {
500
+ if (MODE !== "expert") return error("ci_load_pack requires expert mode");
501
+ const packName = params.pack;
502
+ const packPath = join(PACKS_DIR, `${packName}.json`);
503
+ if (!existsSync(packPath)) {
504
+ const available = existsSync(PACKS_DIR)
505
+ ? readdirSync(PACKS_DIR).filter((f) => f.endsWith(".json")).map((f) => f.replace(".json", ""))
506
+ : [];
507
+ return error(`Unknown pack: ${packName}. Available: ${available.join(", ")}`);
508
+ }
509
+
510
+ const packInstincts = JSON.parse(readFileSync(packPath, "utf8"));
511
+ const existing = readInstincts(project.hash);
512
+ const existingIds = new Set(existing.map((i) => i.id));
513
+ let loaded = 0;
514
+
515
+ for (const inst of packInstincts) {
516
+ if (existingIds.has(inst.id)) continue;
517
+ writeInstinct(project.hash, {
518
+ ...inst,
519
+ source: `pack-${packName}`,
520
+ scope: "project",
521
+ observation_count: 0,
522
+ });
523
+ loaded++;
524
+ }
525
+
526
+ return text(`Loaded ${loaded}/${packInstincts.length} instincts from **${packName}** pack (${packInstincts.length - loaded} already existed)`);
527
+ }
528
+
529
+ default:
530
+ return error(`Unknown tool: ${name}`);
531
+ }
532
+ }
533
+
534
+ function text(t) {
535
+ return { content: [{ type: "text", text: t }] };
536
+ }
537
+
538
+ function error(t) {
539
+ return { content: [{ type: "text", text: t }], isError: true };
540
+ }
541
+
542
+ // ---------------------------------------------------------------------------
543
+ // JSON-RPC stdio transport (zero dependencies)
544
+ // ---------------------------------------------------------------------------
545
+
546
+ const rl = createInterface({ input: process.stdin, terminal: false });
547
+ let buffer = "";
548
+
549
+ rl.on("line", (line) => {
550
+ buffer += line;
551
+ try {
552
+ const msg = JSON.parse(buffer);
553
+ buffer = "";
554
+ handleMessage(msg);
555
+ } catch {
556
+ // incomplete JSON, keep buffering
557
+ }
558
+ });
559
+
560
+ function send(response) {
561
+ const json = JSON.stringify(response);
562
+ process.stdout.write(`Content-Length: ${Buffer.byteLength(json)}\r\n\r\n${json}`);
563
+ }
564
+
565
+ function handleMessage(msg) {
566
+ const { id, method, params } = msg;
567
+
568
+ switch (method) {
569
+ case "initialize":
570
+ send({
571
+ jsonrpc: "2.0",
572
+ id,
573
+ result: {
574
+ protocolVersion: "2024-11-05",
575
+ capabilities: {
576
+ tools: { listChanged: false },
577
+ resources: { subscribe: false, listChanged: false },
578
+ },
579
+ serverInfo: {
580
+ name: "continuous-improvement",
581
+ version: VERSION,
582
+ },
583
+ },
584
+ });
585
+ break;
586
+
587
+ case "notifications/initialized":
588
+ // no response needed
589
+ break;
590
+
591
+ case "tools/list":
592
+ send({
593
+ jsonrpc: "2.0",
594
+ id,
595
+ result: { tools: getAllTools() },
596
+ });
597
+ break;
598
+
599
+ case "tools/call": {
600
+ const result = handleTool(params.name, params.arguments || {});
601
+ send({ jsonrpc: "2.0", id, result });
602
+ break;
603
+ }
604
+
605
+ case "resources/list": {
606
+ const project = getProjectHash();
607
+ const resources = [
608
+ {
609
+ uri: `instincts://project/${project.hash}`,
610
+ name: `${project.name} instincts`,
611
+ description: `Learned instincts for ${project.name}`,
612
+ mimeType: "application/json",
613
+ },
614
+ {
615
+ uri: "instincts://global",
616
+ name: "Global instincts",
617
+ description: "Cross-project instincts",
618
+ mimeType: "application/json",
619
+ },
620
+ ];
621
+ send({ jsonrpc: "2.0", id, result: { resources } });
622
+ break;
623
+ }
624
+
625
+ case "resources/read": {
626
+ const uri = params.uri;
627
+ const project = getProjectHash();
628
+ let instincts;
629
+
630
+ if (uri === "instincts://global") {
631
+ instincts = readInstincts("global").filter((i) => i.scope === "global");
632
+ } else {
633
+ instincts = readInstincts(project.hash);
634
+ }
635
+
636
+ send({
637
+ jsonrpc: "2.0",
638
+ id,
639
+ result: {
640
+ contents: [
641
+ {
642
+ uri,
643
+ text: JSON.stringify(instincts, null, 2),
644
+ mimeType: "application/json",
645
+ },
646
+ ],
647
+ },
648
+ });
649
+ break;
650
+ }
651
+
652
+ default:
653
+ if (id) {
654
+ send({
655
+ jsonrpc: "2.0",
656
+ id,
657
+ error: { code: -32601, message: `Method not found: ${method}` },
658
+ });
659
+ }
660
+ }
661
+ }
662
+
663
+ console.error(`continuous-improvement MCP server v${VERSION} started (mode: ${MODE})`);