ruvnet-kb-first 5.0.0 → 6.0.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 +41 -23
- package/SKILL.md +153 -75
- package/package.json +1 -1
- package/src/mcp-server.js +592 -258
package/src/mcp-server.js
CHANGED
|
@@ -1,364 +1,712 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* RuvNet KB-First MCP Server
|
|
2
|
+
* RuvNet KB-First MCP Server - Score-Driven Architecture
|
|
3
|
+
* Version 6.0.0
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
5
|
+
* PHILOSOPHY: Scoring IS the enforcement mechanism.
|
|
6
|
+
* - Every operation requires baseline score first
|
|
7
|
+
* - Every operation shows delta (before/after)
|
|
8
|
+
* - Hard gates BLOCK on negative deltas
|
|
9
|
+
* - No shortcuts - rigorous measurement drives quality
|
|
10
|
+
*
|
|
11
|
+
* 4 Tools (not 7):
|
|
12
|
+
* 1. kb_first_assess - Calculate baseline scores (KB + App + Process)
|
|
13
|
+
* 2. kb_first_phase - Execute phase work with delta tracking
|
|
14
|
+
* 3. kb_first_delta - Explicit before/after comparison
|
|
15
|
+
* 4. kb_first_gate - Hard gate that blocks on negative delta
|
|
6
16
|
*
|
|
7
17
|
* Usage:
|
|
8
18
|
* npx ruvnet-kb-first mcp
|
|
9
19
|
* node src/mcp-server.js
|
|
10
20
|
*/
|
|
11
21
|
|
|
12
|
-
import {
|
|
13
|
-
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
22
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
14
23
|
import { join } from 'path';
|
|
24
|
+
import { globSync } from 'glob';
|
|
15
25
|
|
|
16
26
|
// MCP Protocol Constants
|
|
17
27
|
const MCP_VERSION = '0.1.0';
|
|
18
28
|
const SERVER_NAME = 'ruvnet-kb-first';
|
|
19
|
-
const SERVER_VERSION = '
|
|
29
|
+
const SERVER_VERSION = '6.0.0';
|
|
20
30
|
|
|
21
31
|
/**
|
|
22
|
-
*
|
|
32
|
+
* Score Categories (total 100 points)
|
|
33
|
+
* These are the ONLY metrics that matter
|
|
23
34
|
*/
|
|
24
|
-
const
|
|
25
|
-
{
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
type: 'boolean',
|
|
33
|
-
description: 'Overwrite existing configuration'
|
|
34
|
-
},
|
|
35
|
-
template: {
|
|
36
|
-
type: 'string',
|
|
37
|
-
enum: ['basic', 'api', 'fullstack'],
|
|
38
|
-
description: 'Project template type'
|
|
39
|
-
}
|
|
40
|
-
}
|
|
35
|
+
const SCORE_WEIGHTS = {
|
|
36
|
+
kb: {
|
|
37
|
+
weight: 40,
|
|
38
|
+
components: {
|
|
39
|
+
entries: 10, // KB has content
|
|
40
|
+
coverage: 10, // Domain coverage completeness
|
|
41
|
+
embeddings: 10, // Vectors generated
|
|
42
|
+
freshness: 10 // Recent updates
|
|
41
43
|
}
|
|
42
44
|
},
|
|
43
|
-
{
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
type: 'boolean',
|
|
51
|
-
description: 'Show detailed breakdown'
|
|
52
|
-
}
|
|
53
|
-
}
|
|
45
|
+
app: {
|
|
46
|
+
weight: 40,
|
|
47
|
+
components: {
|
|
48
|
+
kbCitations: 15, // Code files cite KB sources
|
|
49
|
+
gapResolution: 10, // Gaps identified and resolved
|
|
50
|
+
testCoverage: 10, // Tests exist and pass
|
|
51
|
+
security: 5 // Security basics in place
|
|
54
52
|
}
|
|
55
53
|
},
|
|
54
|
+
process: {
|
|
55
|
+
weight: 20,
|
|
56
|
+
components: {
|
|
57
|
+
phaseCompletion: 10, // Phases properly completed
|
|
58
|
+
gatesPassed: 5, // Hard gates verified
|
|
59
|
+
documentation: 5 // Docs exist
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Phase definitions
|
|
66
|
+
*/
|
|
67
|
+
const PHASES = {
|
|
68
|
+
0: { name: 'Assessment', gate: 'assessment_documented' },
|
|
69
|
+
1: { name: 'KB Design', gate: 'schema_designed' },
|
|
70
|
+
1.5: { name: 'Hooks Setup', gate: 'hooks_verified' },
|
|
71
|
+
2: { name: 'Schema Definition', gate: 'schema_created' },
|
|
72
|
+
3: { name: 'KB Population', gate: 'kb_score_50' },
|
|
73
|
+
4: { name: 'Scoring & Gaps', gate: 'kb_score_80' },
|
|
74
|
+
5: { name: 'Integration', gate: 'integration_tested' },
|
|
75
|
+
6: { name: 'Testing', gate: 'tests_passing' },
|
|
76
|
+
7: { name: 'Optimization', gate: 'performance_met' },
|
|
77
|
+
7.5: { name: 'Testing Gate', gate: 'coverage_80' },
|
|
78
|
+
8: { name: 'Verification', gate: 'all_checks_pass' },
|
|
79
|
+
9: { name: 'Security', gate: 'security_audit_passed' },
|
|
80
|
+
10: { name: 'Documentation', gate: 'docs_complete' },
|
|
81
|
+
11: { name: 'Deployment', gate: 'deployed' },
|
|
82
|
+
11.5: { name: 'Observability', gate: 'monitoring_active' },
|
|
83
|
+
12: { name: 'KB Operations', gate: 'operations_ready' }
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* MCP Tools - Score-Driven Architecture
|
|
88
|
+
*/
|
|
89
|
+
const TOOLS = [
|
|
56
90
|
{
|
|
57
|
-
name: '
|
|
58
|
-
description:
|
|
91
|
+
name: 'kb_first_assess',
|
|
92
|
+
description: `Calculate comprehensive baseline scores for KB, App, and Process.
|
|
93
|
+
ALWAYS RUN THIS FIRST before any work. Returns:
|
|
94
|
+
- KB Score (40 points): entries, coverage, embeddings, freshness
|
|
95
|
+
- App Score (40 points): citations, gap resolution, tests, security
|
|
96
|
+
- Process Score (20 points): phases, gates, documentation
|
|
97
|
+
- Total (100 points)
|
|
98
|
+
|
|
99
|
+
This becomes your BASELINE for delta comparison.`,
|
|
59
100
|
inputSchema: {
|
|
60
101
|
type: 'object',
|
|
61
102
|
properties: {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
description: 'Verify specific phase (0-11)'
|
|
65
|
-
},
|
|
66
|
-
all: {
|
|
67
|
-
type: 'boolean',
|
|
68
|
-
description: 'Run all verification scripts'
|
|
69
|
-
}
|
|
103
|
+
detailed: { type: 'boolean', description: 'Show component breakdown', default: true },
|
|
104
|
+
saveBaseline: { type: 'boolean', description: 'Save as baseline for delta comparison', default: true }
|
|
70
105
|
}
|
|
71
106
|
}
|
|
72
107
|
},
|
|
73
108
|
{
|
|
74
|
-
name: '
|
|
75
|
-
description:
|
|
109
|
+
name: 'kb_first_phase',
|
|
110
|
+
description: `Execute a phase with automatic delta tracking.
|
|
111
|
+
REQUIRES: kb_first_assess must be run first to establish baseline.
|
|
112
|
+
|
|
113
|
+
Workflow:
|
|
114
|
+
1. Loads baseline score from last kb_first_assess
|
|
115
|
+
2. Shows phase requirements and sub-phases
|
|
116
|
+
3. Returns guidance for completing the phase
|
|
117
|
+
4. REMINDS you to run kb_first_delta when done
|
|
118
|
+
|
|
119
|
+
Will WARN if baseline is stale (>1 hour old).`,
|
|
76
120
|
inputSchema: {
|
|
77
121
|
type: 'object',
|
|
78
122
|
properties: {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
}
|
|
123
|
+
phase: { type: 'number', description: 'Phase number (0-12, including 1.5, 7.5, 11.5)' }
|
|
124
|
+
},
|
|
125
|
+
required: ['phase']
|
|
84
126
|
}
|
|
85
127
|
},
|
|
86
128
|
{
|
|
87
|
-
name: '
|
|
88
|
-
description:
|
|
129
|
+
name: 'kb_first_delta',
|
|
130
|
+
description: `Compare current scores against baseline. THE ENFORCEMENT MECHANISM.
|
|
131
|
+
Shows:
|
|
132
|
+
- Baseline score (from kb_first_assess)
|
|
133
|
+
- Current score (calculated now)
|
|
134
|
+
- Delta (+ improvement or - regression)
|
|
135
|
+
- VERDICT: PASS (positive delta) or FAIL (negative delta)
|
|
136
|
+
|
|
137
|
+
If delta is negative, you CANNOT proceed to next phase.
|
|
138
|
+
This prevents shortcuts and enforces rigor.`,
|
|
89
139
|
inputSchema: {
|
|
90
140
|
type: 'object',
|
|
91
141
|
properties: {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
description: 'Phase number (0, 1, 1.5, 2-11)'
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
required: ['phase']
|
|
142
|
+
showBreakdown: { type: 'boolean', description: 'Show which components changed', default: true }
|
|
143
|
+
}
|
|
98
144
|
}
|
|
99
145
|
},
|
|
100
146
|
{
|
|
101
|
-
name: '
|
|
102
|
-
description:
|
|
147
|
+
name: 'kb_first_gate',
|
|
148
|
+
description: `Hard gate check for phase transition.
|
|
149
|
+
BLOCKS progress if:
|
|
150
|
+
- Delta is negative (score dropped)
|
|
151
|
+
- Required gate condition not met
|
|
152
|
+
- Baseline not established
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
- canProceed: boolean
|
|
156
|
+
- blockReason: string (if blocked)
|
|
157
|
+
- nextPhase: number (if can proceed)
|
|
158
|
+
|
|
159
|
+
THIS IS THE HARD GATE. No bypassing.`,
|
|
103
160
|
inputSchema: {
|
|
104
161
|
type: 'object',
|
|
105
162
|
properties: {
|
|
106
|
-
|
|
107
|
-
type: 'string',
|
|
108
|
-
enum: ['install', 'verify', 'train', 'status'],
|
|
109
|
-
description: 'Hook action to perform'
|
|
110
|
-
}
|
|
163
|
+
phase: { type: 'number', description: 'Phase to verify gate for' }
|
|
111
164
|
},
|
|
112
|
-
required: ['
|
|
165
|
+
required: ['phase']
|
|
113
166
|
}
|
|
114
167
|
}
|
|
115
168
|
];
|
|
116
169
|
|
|
117
170
|
/**
|
|
118
|
-
*
|
|
171
|
+
* Calculate all scores
|
|
119
172
|
*/
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
case 'kb_first_verify':
|
|
131
|
-
return await handleVerify(cwd, args);
|
|
173
|
+
function calculateScores(cwd) {
|
|
174
|
+
const scores = {
|
|
175
|
+
kb: { total: 0, max: 40, components: {} },
|
|
176
|
+
app: { total: 0, max: 40, components: {} },
|
|
177
|
+
process: { total: 0, max: 20, components: {} },
|
|
178
|
+
total: 0,
|
|
179
|
+
max: 100,
|
|
180
|
+
grade: 'F',
|
|
181
|
+
timestamp: new Date().toISOString()
|
|
182
|
+
};
|
|
132
183
|
|
|
133
|
-
|
|
134
|
-
|
|
184
|
+
// ===== KB SCORE (40 points) =====
|
|
185
|
+
const ruvectorDir = join(cwd, '.ruvector');
|
|
186
|
+
const kbDir = join(cwd, 'src', 'kb');
|
|
187
|
+
|
|
188
|
+
// KB Entries (10 points)
|
|
189
|
+
let kbEntries = 0;
|
|
190
|
+
if (existsSync(kbDir)) {
|
|
191
|
+
try {
|
|
192
|
+
const files = readdirSync(kbDir);
|
|
193
|
+
kbEntries = files.length;
|
|
194
|
+
} catch {}
|
|
195
|
+
}
|
|
196
|
+
scores.kb.components.entries = Math.min(10, Math.floor(kbEntries / 5) * 2);
|
|
197
|
+
|
|
198
|
+
// KB Coverage (10 points) - based on documented domains
|
|
199
|
+
const docsDir = join(cwd, 'docs');
|
|
200
|
+
let domainDocs = 0;
|
|
201
|
+
if (existsSync(docsDir)) {
|
|
202
|
+
try {
|
|
203
|
+
const files = readdirSync(docsDir);
|
|
204
|
+
domainDocs = files.filter(f => f.endsWith('.md')).length;
|
|
205
|
+
} catch {}
|
|
206
|
+
}
|
|
207
|
+
scores.kb.components.coverage = Math.min(10, domainDocs * 2);
|
|
208
|
+
|
|
209
|
+
// KB Embeddings (10 points) - check for vector files or config
|
|
210
|
+
const configPath = join(ruvectorDir, 'config.json');
|
|
211
|
+
let hasEmbeddings = false;
|
|
212
|
+
if (existsSync(configPath)) {
|
|
213
|
+
try {
|
|
214
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
215
|
+
hasEmbeddings = config.kbFirst?.embeddings === true || kbEntries > 0;
|
|
216
|
+
} catch {}
|
|
217
|
+
}
|
|
218
|
+
scores.kb.components.embeddings = hasEmbeddings ? 10 : 0;
|
|
219
|
+
|
|
220
|
+
// KB Freshness (10 points) - recent updates
|
|
221
|
+
let freshness = 0;
|
|
222
|
+
if (existsSync(ruvectorDir)) {
|
|
223
|
+
try {
|
|
224
|
+
const stat = statSync(ruvectorDir);
|
|
225
|
+
const daysSinceUpdate = (Date.now() - stat.mtime.getTime()) / (1000 * 60 * 60 * 24);
|
|
226
|
+
if (daysSinceUpdate < 1) freshness = 10;
|
|
227
|
+
else if (daysSinceUpdate < 7) freshness = 7;
|
|
228
|
+
else if (daysSinceUpdate < 30) freshness = 4;
|
|
229
|
+
else freshness = 0;
|
|
230
|
+
} catch {}
|
|
231
|
+
}
|
|
232
|
+
scores.kb.components.freshness = freshness;
|
|
233
|
+
|
|
234
|
+
scores.kb.total = Object.values(scores.kb.components).reduce((a, b) => a + b, 0);
|
|
235
|
+
|
|
236
|
+
// ===== APP SCORE (40 points) =====
|
|
237
|
+
const srcDir = join(cwd, 'src');
|
|
238
|
+
|
|
239
|
+
// KB Citations (15 points)
|
|
240
|
+
let codeFiles = [];
|
|
241
|
+
let filesWithCitation = 0;
|
|
242
|
+
if (existsSync(srcDir)) {
|
|
243
|
+
try {
|
|
244
|
+
codeFiles = globSync('**/*.{ts,tsx,js,jsx,py,go,rs}', { cwd: srcDir });
|
|
245
|
+
for (const file of codeFiles) {
|
|
246
|
+
try {
|
|
247
|
+
const content = readFileSync(join(srcDir, file), 'utf-8');
|
|
248
|
+
if (content.includes('KB-Generated:') || content.includes('Sources:') || content.includes('@kb-source')) {
|
|
249
|
+
filesWithCitation++;
|
|
250
|
+
}
|
|
251
|
+
} catch {}
|
|
252
|
+
}
|
|
253
|
+
} catch {}
|
|
254
|
+
}
|
|
255
|
+
const citationPercent = codeFiles.length > 0 ? filesWithCitation / codeFiles.length : 1;
|
|
256
|
+
scores.app.components.kbCitations = Math.round(citationPercent * 15);
|
|
257
|
+
|
|
258
|
+
// Gap Resolution (10 points)
|
|
259
|
+
const gapsPath = join(ruvectorDir, 'gaps.jsonl');
|
|
260
|
+
let gapCount = 0;
|
|
261
|
+
if (existsSync(gapsPath)) {
|
|
262
|
+
try {
|
|
263
|
+
const content = readFileSync(gapsPath, 'utf-8').trim();
|
|
264
|
+
gapCount = content ? content.split('\n').length : 0;
|
|
265
|
+
} catch {}
|
|
266
|
+
}
|
|
267
|
+
scores.app.components.gapResolution = Math.max(0, 10 - gapCount);
|
|
268
|
+
|
|
269
|
+
// Test Coverage (10 points)
|
|
270
|
+
let hasTests = false;
|
|
271
|
+
const testDirs = ['tests', 'test', '__tests__', 'src/__tests__'];
|
|
272
|
+
for (const td of testDirs) {
|
|
273
|
+
if (existsSync(join(cwd, td))) {
|
|
274
|
+
hasTests = true;
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const testFiles = existsSync(srcDir) ? globSync('**/*.{test,spec}.{ts,tsx,js,jsx}', { cwd: srcDir }) : [];
|
|
279
|
+
scores.app.components.testCoverage = hasTests ? 5 : 0;
|
|
280
|
+
scores.app.components.testCoverage += Math.min(5, testFiles.length);
|
|
281
|
+
|
|
282
|
+
// Security (5 points)
|
|
283
|
+
let secScore = 5;
|
|
284
|
+
const gitignorePath = join(cwd, '.gitignore');
|
|
285
|
+
if (existsSync(gitignorePath)) {
|
|
286
|
+
try {
|
|
287
|
+
const content = readFileSync(gitignorePath, 'utf-8');
|
|
288
|
+
if (!content.includes('.env')) secScore -= 2;
|
|
289
|
+
if (!content.includes('node_modules')) secScore -= 1;
|
|
290
|
+
} catch {}
|
|
291
|
+
} else {
|
|
292
|
+
secScore -= 3;
|
|
293
|
+
}
|
|
294
|
+
scores.app.components.security = Math.max(0, secScore);
|
|
135
295
|
|
|
136
|
-
|
|
137
|
-
return await handlePhase(cwd, args);
|
|
296
|
+
scores.app.total = Object.values(scores.app.components).reduce((a, b) => a + b, 0);
|
|
138
297
|
|
|
139
|
-
|
|
140
|
-
return await handleHooks(cwd, args);
|
|
298
|
+
// ===== PROCESS SCORE (20 points) =====
|
|
141
299
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
300
|
+
// Phase Completion (10 points)
|
|
301
|
+
let completedPhases = [];
|
|
302
|
+
if (existsSync(configPath)) {
|
|
303
|
+
try {
|
|
304
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
305
|
+
completedPhases = config.phases?.completed || [];
|
|
306
|
+
} catch {}
|
|
146
307
|
}
|
|
308
|
+
const totalPhases = Object.keys(PHASES).length;
|
|
309
|
+
scores.process.components.phaseCompletion = Math.round((completedPhases.length / totalPhases) * 10);
|
|
310
|
+
|
|
311
|
+
// Gates Passed (5 points)
|
|
312
|
+
let gatesPassed = 0;
|
|
313
|
+
if (existsSync(configPath)) {
|
|
314
|
+
try {
|
|
315
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
316
|
+
gatesPassed = Object.values(config.phases?.gates || {}).filter(v => v === true).length;
|
|
317
|
+
} catch {}
|
|
318
|
+
}
|
|
319
|
+
scores.process.components.gatesPassed = Math.min(5, Math.round((gatesPassed / totalPhases) * 5));
|
|
320
|
+
|
|
321
|
+
// Documentation (5 points)
|
|
322
|
+
let docScore = 0;
|
|
323
|
+
if (existsSync(join(cwd, 'README.md'))) docScore += 2;
|
|
324
|
+
if (existsSync(join(cwd, 'docs', 'api.md')) || existsSync(join(cwd, 'docs', 'API.md'))) docScore += 1;
|
|
325
|
+
if (existsSync(join(cwd, 'docs', 'architecture.md'))) docScore += 1;
|
|
326
|
+
if (existsSync(join(cwd, 'CHANGELOG.md'))) docScore += 1;
|
|
327
|
+
scores.process.components.documentation = Math.min(5, docScore);
|
|
328
|
+
|
|
329
|
+
scores.process.total = Object.values(scores.process.components).reduce((a, b) => a + b, 0);
|
|
330
|
+
|
|
331
|
+
// ===== TOTAL =====
|
|
332
|
+
scores.total = scores.kb.total + scores.app.total + scores.process.total;
|
|
333
|
+
|
|
334
|
+
// Grade
|
|
335
|
+
if (scores.total >= 98) scores.grade = 'A+';
|
|
336
|
+
else if (scores.total >= 93) scores.grade = 'A';
|
|
337
|
+
else if (scores.total >= 90) scores.grade = 'A-';
|
|
338
|
+
else if (scores.total >= 87) scores.grade = 'B+';
|
|
339
|
+
else if (scores.total >= 83) scores.grade = 'B';
|
|
340
|
+
else if (scores.total >= 80) scores.grade = 'B-';
|
|
341
|
+
else if (scores.total >= 70) scores.grade = 'C';
|
|
342
|
+
else if (scores.total >= 60) scores.grade = 'D';
|
|
343
|
+
else scores.grade = 'F';
|
|
344
|
+
|
|
345
|
+
return scores;
|
|
147
346
|
}
|
|
148
347
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
348
|
+
/**
|
|
349
|
+
* Tool Handlers
|
|
350
|
+
*/
|
|
351
|
+
async function handleKbFirstAssess(cwd, args) {
|
|
352
|
+
const scores = calculateScores(cwd);
|
|
353
|
+
|
|
354
|
+
// Save baseline
|
|
355
|
+
if (args.saveBaseline !== false) {
|
|
356
|
+
const ruvectorDir = join(cwd, '.ruvector');
|
|
357
|
+
if (!existsSync(ruvectorDir)) {
|
|
358
|
+
mkdirSync(ruvectorDir, { recursive: true });
|
|
359
|
+
}
|
|
360
|
+
writeFileSync(join(ruvectorDir, 'baseline.json'), JSON.stringify(scores, null, 2));
|
|
361
|
+
}
|
|
155
362
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
363
|
+
const result = {
|
|
364
|
+
action: 'BASELINE_ESTABLISHED',
|
|
365
|
+
timestamp: scores.timestamp,
|
|
366
|
+
total: scores.total,
|
|
367
|
+
max: scores.max,
|
|
368
|
+
grade: scores.grade,
|
|
369
|
+
summary: {
|
|
370
|
+
kb: `${scores.kb.total}/${scores.kb.max}`,
|
|
371
|
+
app: `${scores.app.total}/${scores.app.max}`,
|
|
372
|
+
process: `${scores.process.total}/${scores.process.max}`
|
|
373
|
+
},
|
|
374
|
+
nextStep: 'Run kb_first_phase to begin work, then kb_first_delta to measure improvement'
|
|
375
|
+
};
|
|
162
376
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
configPath
|
|
169
|
-
};
|
|
170
|
-
} catch (error) {
|
|
171
|
-
return {
|
|
172
|
-
success: false,
|
|
173
|
-
error: error.message
|
|
377
|
+
if (args.detailed !== false) {
|
|
378
|
+
result.breakdown = {
|
|
379
|
+
kb: scores.kb.components,
|
|
380
|
+
app: scores.app.components,
|
|
381
|
+
process: scores.process.components
|
|
174
382
|
};
|
|
175
383
|
}
|
|
384
|
+
|
|
385
|
+
return result;
|
|
176
386
|
}
|
|
177
387
|
|
|
178
|
-
async function
|
|
179
|
-
const
|
|
388
|
+
async function handleKbFirstPhase(cwd, args) {
|
|
389
|
+
const phase = args.phase;
|
|
390
|
+
const phaseInfo = PHASES[phase];
|
|
180
391
|
|
|
181
|
-
if (!
|
|
392
|
+
if (!phaseInfo) {
|
|
182
393
|
return {
|
|
183
|
-
|
|
184
|
-
|
|
394
|
+
error: `Unknown phase: ${phase}`,
|
|
395
|
+
validPhases: Object.entries(PHASES).map(([k, v]) => ({ phase: parseFloat(k), name: v.name }))
|
|
185
396
|
};
|
|
186
397
|
}
|
|
187
398
|
|
|
188
|
-
//
|
|
189
|
-
const
|
|
399
|
+
// Check for baseline
|
|
400
|
+
const baselinePath = join(cwd, '.ruvector', 'baseline.json');
|
|
401
|
+
let baseline = null;
|
|
402
|
+
let baselineWarning = null;
|
|
403
|
+
|
|
404
|
+
if (existsSync(baselinePath)) {
|
|
405
|
+
try {
|
|
406
|
+
baseline = JSON.parse(readFileSync(baselinePath, 'utf-8'));
|
|
407
|
+
const baselineAge = (Date.now() - new Date(baseline.timestamp).getTime()) / (1000 * 60);
|
|
408
|
+
if (baselineAge > 60) {
|
|
409
|
+
baselineWarning = `Baseline is ${Math.round(baselineAge)} minutes old. Consider running kb_first_assess for fresh baseline.`;
|
|
410
|
+
}
|
|
411
|
+
} catch {}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Phase-specific guidance
|
|
415
|
+
const phaseGuidance = {
|
|
416
|
+
0: ['Document project scope', 'Identify domain complexity', 'Assess KB-First suitability', 'Estimate resources', 'Make go/no-go decision'],
|
|
417
|
+
1: ['Map domain concepts', 'Design taxonomy', 'Define relationships', 'Plan query patterns', 'Review with stakeholders'],
|
|
418
|
+
1.5: ['Install enforcement hooks', 'Configure hook behavior', 'Train on project patterns', 'Verify hooks work'],
|
|
419
|
+
2: ['Create database tables', 'Add vector columns', 'Design indexes', 'Write migration scripts'],
|
|
420
|
+
3: ['Collect domain content', 'Process and clean data', 'Generate embeddings', 'Import to KB', 'Validate entries'],
|
|
421
|
+
4: ['Analyze KB coverage', 'Calculate quality score', 'Identify gaps', 'Prioritize fixes', 'Create remediation plan'],
|
|
422
|
+
5: ['Build search API', 'Implement code generation', 'Add citation system', 'Enable gap logging'],
|
|
423
|
+
6: ['Write unit tests', 'Create integration tests', 'Test KB accuracy', 'Performance tests', 'Edge case testing'],
|
|
424
|
+
7: ['Optimize queries', 'Tune indexes', 'Add caching', 'Run benchmarks'],
|
|
425
|
+
7.5: ['Verify test coverage ≥80%', 'Run E2E suite', 'Load testing', 'Build regression suite'],
|
|
426
|
+
8: ['Run code scan', 'Check imports', 'Verify source returns', 'Test startup', 'Check fallbacks', 'Validate attribution', 'Test confidence', 'Review gap logs'],
|
|
427
|
+
9: ['Audit dependencies', 'Check OWASP Top 10', 'Test SQL injection', 'Review auth', 'Audit secrets', 'Secure APIs'],
|
|
428
|
+
10: ['Write README', 'Document API', 'Schema documentation', 'Architecture docs', 'Operator guide'],
|
|
429
|
+
11: ['Setup infrastructure', 'Configure environments', 'Build CI/CD', 'Run migrations', 'Setup monitoring', 'Go live'],
|
|
430
|
+
11.5: ['Setup OpenTelemetry', 'Build KB dashboard', 'Configure alerts', 'Write runbooks'],
|
|
431
|
+
12: ['Define gap triage', 'Setup expert review', 'Document KB updates', 'Version control', 'A/B testing']
|
|
432
|
+
};
|
|
190
433
|
|
|
191
|
-
// Return score data
|
|
192
434
|
return {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
435
|
+
phase,
|
|
436
|
+
name: phaseInfo.name,
|
|
437
|
+
gate: phaseInfo.gate,
|
|
438
|
+
baseline: baseline ? {
|
|
439
|
+
score: baseline.total,
|
|
440
|
+
grade: baseline.grade,
|
|
441
|
+
timestamp: baseline.timestamp
|
|
442
|
+
} : null,
|
|
443
|
+
baselineWarning,
|
|
444
|
+
tasks: phaseGuidance[phase] || [],
|
|
445
|
+
reminder: '⚠️ IMPORTANT: Run kb_first_delta when phase work is complete to measure improvement',
|
|
446
|
+
gateRequirement: `Gate "${phaseInfo.gate}" must be satisfied to proceed`
|
|
196
447
|
};
|
|
197
448
|
}
|
|
198
449
|
|
|
199
|
-
async function
|
|
200
|
-
const
|
|
450
|
+
async function handleKbFirstDelta(cwd, args) {
|
|
451
|
+
const baselinePath = join(cwd, '.ruvector', 'baseline.json');
|
|
201
452
|
|
|
202
|
-
if (!existsSync(
|
|
453
|
+
if (!existsSync(baselinePath)) {
|
|
203
454
|
return {
|
|
204
|
-
|
|
205
|
-
|
|
455
|
+
error: 'NO_BASELINE',
|
|
456
|
+
message: 'No baseline found. Run kb_first_assess first to establish baseline.',
|
|
457
|
+
action: 'Run kb_first_assess with saveBaseline=true'
|
|
206
458
|
};
|
|
207
459
|
}
|
|
208
460
|
|
|
209
|
-
|
|
210
|
-
|
|
461
|
+
let baseline;
|
|
462
|
+
try {
|
|
463
|
+
baseline = JSON.parse(readFileSync(baselinePath, 'utf-8'));
|
|
464
|
+
} catch {
|
|
465
|
+
return { error: 'CORRUPT_BASELINE', message: 'Baseline file is corrupt. Run kb_first_assess again.' };
|
|
466
|
+
}
|
|
211
467
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
hint: 'Verification scripts require terminal execution'
|
|
468
|
+
const current = calculateScores(cwd);
|
|
469
|
+
|
|
470
|
+
const delta = {
|
|
471
|
+
total: current.total - baseline.total,
|
|
472
|
+
kb: current.kb.total - baseline.kb.total,
|
|
473
|
+
app: current.app.total - baseline.app.total,
|
|
474
|
+
process: current.process.total - baseline.process.total
|
|
220
475
|
};
|
|
221
|
-
}
|
|
222
476
|
|
|
223
|
-
|
|
224
|
-
const
|
|
477
|
+
const verdict = delta.total >= 0 ? 'PASS' : 'FAIL';
|
|
478
|
+
const canProceed = delta.total >= 0;
|
|
479
|
+
|
|
480
|
+
const result = {
|
|
481
|
+
verdict,
|
|
482
|
+
canProceed,
|
|
483
|
+
baseline: {
|
|
484
|
+
score: baseline.total,
|
|
485
|
+
grade: baseline.grade,
|
|
486
|
+
timestamp: baseline.timestamp
|
|
487
|
+
},
|
|
488
|
+
current: {
|
|
489
|
+
score: current.total,
|
|
490
|
+
grade: current.grade,
|
|
491
|
+
timestamp: current.timestamp
|
|
492
|
+
},
|
|
493
|
+
delta: {
|
|
494
|
+
total: delta.total > 0 ? `+${delta.total}` : `${delta.total}`,
|
|
495
|
+
kb: delta.kb > 0 ? `+${delta.kb}` : `${delta.kb}`,
|
|
496
|
+
app: delta.app > 0 ? `+${delta.app}` : `${delta.app}`,
|
|
497
|
+
process: delta.process > 0 ? `+${delta.process}` : `${delta.process}`
|
|
498
|
+
}
|
|
499
|
+
};
|
|
225
500
|
|
|
226
|
-
if (!
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
501
|
+
if (!canProceed) {
|
|
502
|
+
result.blockReason = `Score dropped by ${Math.abs(delta.total)} points. You CANNOT proceed until score improves.`;
|
|
503
|
+
result.action = 'Fix issues causing score regression, then run kb_first_delta again.';
|
|
504
|
+
} else {
|
|
505
|
+
result.action = 'Run kb_first_gate to verify phase completion and proceed.';
|
|
231
506
|
}
|
|
232
507
|
|
|
233
|
-
|
|
508
|
+
if (args.showBreakdown !== false) {
|
|
509
|
+
result.componentChanges = {
|
|
510
|
+
kb: {
|
|
511
|
+
before: baseline.kb.components,
|
|
512
|
+
after: current.kb.components
|
|
513
|
+
},
|
|
514
|
+
app: {
|
|
515
|
+
before: baseline.app.components,
|
|
516
|
+
after: current.app.components
|
|
517
|
+
},
|
|
518
|
+
process: {
|
|
519
|
+
before: baseline.process.components,
|
|
520
|
+
after: current.process.components
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
}
|
|
234
524
|
|
|
235
|
-
return
|
|
236
|
-
initialized: true,
|
|
237
|
-
namespace: config.kbFirst?.namespace,
|
|
238
|
-
version: config.kbFirst?.version,
|
|
239
|
-
currentPhase: config.phases?.current || 0,
|
|
240
|
-
completedPhases: config.phases?.completed || [],
|
|
241
|
-
hooksEnabled: config.hooks?.enabled || false
|
|
242
|
-
};
|
|
525
|
+
return result;
|
|
243
526
|
}
|
|
244
527
|
|
|
245
|
-
async function
|
|
246
|
-
const PHASES = {
|
|
247
|
-
0: { name: 'Assessment', subphases: 5 },
|
|
248
|
-
1: { name: 'KB Design', subphases: 5 },
|
|
249
|
-
1.5: { name: 'Hooks Setup', subphases: 4 },
|
|
250
|
-
2: { name: 'Schema Definition', subphases: 4 },
|
|
251
|
-
3: { name: 'KB Population', subphases: 5 },
|
|
252
|
-
4: { name: 'Scoring & Gaps', subphases: 5 },
|
|
253
|
-
5: { name: 'Integration', subphases: 4 },
|
|
254
|
-
6: { name: 'Testing', subphases: 5 },
|
|
255
|
-
7: { name: 'Optimization', subphases: 4 },
|
|
256
|
-
8: { name: 'Verification', subphases: 8 },
|
|
257
|
-
9: { name: 'Security', subphases: 6 },
|
|
258
|
-
10: { name: 'Documentation', subphases: 6 },
|
|
259
|
-
11: { name: 'Deployment', subphases: 6 }
|
|
260
|
-
};
|
|
261
|
-
|
|
528
|
+
async function handleKbFirstGate(cwd, args) {
|
|
262
529
|
const phase = args.phase;
|
|
263
530
|
const phaseInfo = PHASES[phase];
|
|
264
531
|
|
|
265
532
|
if (!phaseInfo) {
|
|
533
|
+
return { error: `Unknown phase: ${phase}` };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Check baseline exists
|
|
537
|
+
const baselinePath = join(cwd, '.ruvector', 'baseline.json');
|
|
538
|
+
if (!existsSync(baselinePath)) {
|
|
266
539
|
return {
|
|
267
|
-
|
|
268
|
-
|
|
540
|
+
canProceed: false,
|
|
541
|
+
blockReason: 'GATE_BLOCKED: No baseline established. Run kb_first_assess first.',
|
|
542
|
+
phase,
|
|
543
|
+
phaseName: phaseInfo.name
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Check delta
|
|
548
|
+
let baseline;
|
|
549
|
+
try {
|
|
550
|
+
baseline = JSON.parse(readFileSync(baselinePath, 'utf-8'));
|
|
551
|
+
} catch {
|
|
552
|
+
return {
|
|
553
|
+
canProceed: false,
|
|
554
|
+
blockReason: 'GATE_BLOCKED: Corrupt baseline. Run kb_first_assess again.',
|
|
555
|
+
phase,
|
|
556
|
+
phaseName: phaseInfo.name
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const current = calculateScores(cwd);
|
|
561
|
+
const delta = current.total - baseline.total;
|
|
562
|
+
|
|
563
|
+
if (delta < 0) {
|
|
564
|
+
return {
|
|
565
|
+
canProceed: false,
|
|
566
|
+
blockReason: `GATE_BLOCKED: Score regression detected (${delta} points). Fix issues before proceeding.`,
|
|
567
|
+
phase,
|
|
568
|
+
phaseName: phaseInfo.name,
|
|
569
|
+
baseline: baseline.total,
|
|
570
|
+
current: current.total,
|
|
571
|
+
delta
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Check gate-specific conditions
|
|
576
|
+
const configPath = join(cwd, '.ruvector', 'config.json');
|
|
577
|
+
let config = { phases: { current: 0, completed: [], gates: {} } };
|
|
578
|
+
if (existsSync(configPath)) {
|
|
579
|
+
try {
|
|
580
|
+
config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
581
|
+
} catch {}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Phase-specific gate checks
|
|
585
|
+
let gateConditionMet = false;
|
|
586
|
+
let gateMessage = '';
|
|
587
|
+
|
|
588
|
+
switch (phaseInfo.gate) {
|
|
589
|
+
case 'kb_score_50':
|
|
590
|
+
gateConditionMet = current.kb.total >= 20; // 50% of 40
|
|
591
|
+
gateMessage = gateConditionMet ? 'KB score ≥50%' : `KB score ${current.kb.total}/40 < 50%`;
|
|
592
|
+
break;
|
|
593
|
+
case 'kb_score_80':
|
|
594
|
+
gateConditionMet = current.kb.total >= 32; // 80% of 40
|
|
595
|
+
gateMessage = gateConditionMet ? 'KB score ≥80%' : `KB score ${current.kb.total}/40 < 80%`;
|
|
596
|
+
break;
|
|
597
|
+
case 'coverage_80':
|
|
598
|
+
gateConditionMet = current.app.components.testCoverage >= 8; // 80% of 10
|
|
599
|
+
gateMessage = gateConditionMet ? 'Test coverage ≥80%' : `Test coverage ${current.app.components.testCoverage}/10 < 80%`;
|
|
600
|
+
break;
|
|
601
|
+
default:
|
|
602
|
+
// Default: pass if delta is non-negative
|
|
603
|
+
gateConditionMet = true;
|
|
604
|
+
gateMessage = 'Gate condition satisfied (positive delta)';
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (!gateConditionMet) {
|
|
608
|
+
return {
|
|
609
|
+
canProceed: false,
|
|
610
|
+
blockReason: `GATE_BLOCKED: ${gateMessage}`,
|
|
611
|
+
phase,
|
|
612
|
+
phaseName: phaseInfo.name,
|
|
613
|
+
gate: phaseInfo.gate
|
|
269
614
|
};
|
|
270
615
|
}
|
|
271
616
|
|
|
617
|
+
// Mark phase as completed
|
|
618
|
+
if (!config.phases.completed.includes(parseFloat(phase))) {
|
|
619
|
+
config.phases.completed.push(parseFloat(phase));
|
|
620
|
+
}
|
|
621
|
+
config.phases.gates[phaseInfo.gate] = true;
|
|
622
|
+
|
|
623
|
+
// Determine next phase
|
|
624
|
+
const phaseOrder = [0, 1, 1.5, 2, 3, 4, 5, 6, 7, 7.5, 8, 9, 10, 11, 11.5, 12];
|
|
625
|
+
const currentIdx = phaseOrder.indexOf(parseFloat(phase));
|
|
626
|
+
const nextPhase = currentIdx < phaseOrder.length - 1 ? phaseOrder[currentIdx + 1] : null;
|
|
627
|
+
|
|
628
|
+
if (nextPhase !== null) {
|
|
629
|
+
config.phases.current = nextPhase;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Save config
|
|
633
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
634
|
+
|
|
635
|
+
// Save current scores as new baseline for next phase
|
|
636
|
+
writeFileSync(baselinePath, JSON.stringify(current, null, 2));
|
|
637
|
+
|
|
272
638
|
return {
|
|
639
|
+
canProceed: true,
|
|
273
640
|
phase,
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
641
|
+
phaseName: phaseInfo.name,
|
|
642
|
+
gateStatus: 'PASSED',
|
|
643
|
+
gateMessage,
|
|
644
|
+
scoreImprovement: delta > 0 ? `+${delta}` : '0',
|
|
645
|
+
nextPhase,
|
|
646
|
+
nextPhaseName: nextPhase !== null ? PHASES[nextPhase]?.name : 'PROJECT COMPLETE',
|
|
647
|
+
action: nextPhase !== null
|
|
648
|
+
? `Run kb_first_assess to establish baseline for Phase ${nextPhase}: ${PHASES[nextPhase]?.name}`
|
|
649
|
+
: 'All phases complete! Project ready for production.'
|
|
277
650
|
};
|
|
278
651
|
}
|
|
279
652
|
|
|
280
|
-
|
|
281
|
-
|
|
653
|
+
/**
|
|
654
|
+
* Handle MCP tool calls
|
|
655
|
+
*/
|
|
656
|
+
async function handleToolCall(toolName, args) {
|
|
657
|
+
const cwd = process.cwd();
|
|
282
658
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
659
|
+
switch (toolName) {
|
|
660
|
+
case 'kb_first_assess':
|
|
661
|
+
return await handleKbFirstAssess(cwd, args);
|
|
662
|
+
case 'kb_first_phase':
|
|
663
|
+
return await handleKbFirstPhase(cwd, args);
|
|
664
|
+
case 'kb_first_delta':
|
|
665
|
+
return await handleKbFirstDelta(cwd, args);
|
|
666
|
+
case 'kb_first_gate':
|
|
667
|
+
return await handleKbFirstGate(cwd, args);
|
|
668
|
+
default:
|
|
669
|
+
return { error: `Unknown tool: ${toolName}` };
|
|
670
|
+
}
|
|
289
671
|
}
|
|
290
672
|
|
|
291
673
|
/**
|
|
292
674
|
* MCP Protocol Handler
|
|
293
675
|
*/
|
|
294
|
-
function handleMCPMessage(message) {
|
|
676
|
+
async function handleMCPMessage(message) {
|
|
295
677
|
const { jsonrpc, id, method, params } = message;
|
|
296
678
|
|
|
297
679
|
if (jsonrpc !== '2.0') {
|
|
298
|
-
return {
|
|
299
|
-
jsonrpc: '2.0',
|
|
300
|
-
id,
|
|
301
|
-
error: { code: -32600, message: 'Invalid JSON-RPC version' }
|
|
302
|
-
};
|
|
680
|
+
return { jsonrpc: '2.0', id, error: { code: -32600, message: 'Invalid JSON-RPC version' } };
|
|
303
681
|
}
|
|
304
682
|
|
|
305
683
|
switch (method) {
|
|
306
684
|
case 'initialize':
|
|
307
685
|
return {
|
|
308
|
-
jsonrpc: '2.0',
|
|
309
|
-
id,
|
|
686
|
+
jsonrpc: '2.0', id,
|
|
310
687
|
result: {
|
|
311
688
|
protocolVersion: MCP_VERSION,
|
|
312
|
-
serverInfo: {
|
|
313
|
-
|
|
314
|
-
version: SERVER_VERSION
|
|
315
|
-
},
|
|
316
|
-
capabilities: {
|
|
317
|
-
tools: {}
|
|
318
|
-
}
|
|
689
|
+
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
|
|
690
|
+
capabilities: { tools: {} }
|
|
319
691
|
}
|
|
320
692
|
};
|
|
321
693
|
|
|
322
694
|
case 'tools/list':
|
|
323
|
-
return {
|
|
324
|
-
jsonrpc: '2.0',
|
|
325
|
-
id,
|
|
326
|
-
result: { tools: TOOLS }
|
|
327
|
-
};
|
|
695
|
+
return { jsonrpc: '2.0', id, result: { tools: TOOLS } };
|
|
328
696
|
|
|
329
697
|
case 'tools/call':
|
|
330
|
-
|
|
698
|
+
try {
|
|
699
|
+
const result = await handleToolCall(params.name, params.arguments || {});
|
|
700
|
+
return {
|
|
701
|
+
jsonrpc: '2.0', id,
|
|
702
|
+
result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
|
703
|
+
};
|
|
704
|
+
} catch (error) {
|
|
705
|
+
return { jsonrpc: '2.0', id, error: { code: -32000, message: error.message } };
|
|
706
|
+
}
|
|
331
707
|
|
|
332
708
|
default:
|
|
333
|
-
return {
|
|
334
|
-
jsonrpc: '2.0',
|
|
335
|
-
id,
|
|
336
|
-
error: { code: -32601, message: `Unknown method: ${method}` }
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
async function handleToolCallAsync(id, toolName, args) {
|
|
342
|
-
try {
|
|
343
|
-
const result = await handleToolCall(toolName, args || {});
|
|
344
|
-
return {
|
|
345
|
-
jsonrpc: '2.0',
|
|
346
|
-
id,
|
|
347
|
-
result: {
|
|
348
|
-
content: [
|
|
349
|
-
{
|
|
350
|
-
type: 'text',
|
|
351
|
-
text: JSON.stringify(result, null, 2)
|
|
352
|
-
}
|
|
353
|
-
]
|
|
354
|
-
}
|
|
355
|
-
};
|
|
356
|
-
} catch (error) {
|
|
357
|
-
return {
|
|
358
|
-
jsonrpc: '2.0',
|
|
359
|
-
id,
|
|
360
|
-
error: { code: -32000, message: error.message }
|
|
361
|
-
};
|
|
709
|
+
return { jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown method: ${method}` } };
|
|
362
710
|
}
|
|
363
711
|
}
|
|
364
712
|
|
|
@@ -366,47 +714,33 @@ async function handleToolCallAsync(id, toolName, args) {
|
|
|
366
714
|
* Start MCP Server (stdio mode)
|
|
367
715
|
*/
|
|
368
716
|
export async function startMCPServer(options = {}) {
|
|
369
|
-
console.error(
|
|
370
|
-
console.error(
|
|
371
|
-
console.error('
|
|
717
|
+
console.error(`RuvNet KB-First MCP Server v${SERVER_VERSION}`);
|
|
718
|
+
console.error('Architecture: Score-Driven | Tools: 4 | Phases: 15');
|
|
719
|
+
console.error('Philosophy: Scoring IS enforcement. No shortcuts.');
|
|
372
720
|
|
|
373
|
-
// Read from stdin
|
|
374
721
|
let buffer = '';
|
|
375
|
-
|
|
376
722
|
process.stdin.setEncoding('utf-8');
|
|
723
|
+
|
|
377
724
|
process.stdin.on('data', async (chunk) => {
|
|
378
725
|
buffer += chunk;
|
|
379
|
-
|
|
380
|
-
// Try to parse complete JSON-RPC messages
|
|
381
726
|
const lines = buffer.split('\n');
|
|
382
727
|
buffer = lines.pop() || '';
|
|
383
728
|
|
|
384
729
|
for (const line of lines) {
|
|
385
730
|
if (!line.trim()) continue;
|
|
386
|
-
|
|
387
731
|
try {
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
if (response) {
|
|
392
|
-
process.stdout.write(JSON.stringify(response) + '\n');
|
|
393
|
-
}
|
|
732
|
+
const response = await handleMCPMessage(JSON.parse(line));
|
|
733
|
+
if (response) process.stdout.write(JSON.stringify(response) + '\n');
|
|
394
734
|
} catch (error) {
|
|
395
735
|
console.error('Parse error:', error.message);
|
|
396
736
|
}
|
|
397
737
|
}
|
|
398
738
|
});
|
|
399
739
|
|
|
400
|
-
process.stdin.on('end', () =>
|
|
401
|
-
console.error('MCP Server shutting down');
|
|
402
|
-
process.exit(0);
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
// Keep process alive
|
|
740
|
+
process.stdin.on('end', () => process.exit(0));
|
|
406
741
|
process.stdin.resume();
|
|
407
742
|
}
|
|
408
743
|
|
|
409
|
-
// Run if called directly
|
|
410
744
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
411
745
|
startMCPServer();
|
|
412
746
|
}
|