@redaksjon/brennpunkt 0.0.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/dist/main.js +1 -1
- package/dist/mcp/server.js +688 -3
- package/dist/mcp/server.js.map +1 -1
- package/guide/ai-integration.md +42 -0
- package/package.json +10 -4
package/dist/mcp/server.js
CHANGED
|
@@ -1,11 +1,662 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
-
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
5
|
import { existsSync, statSync, readFileSync } from 'node:fs';
|
|
6
6
|
import { resolve } from 'node:path';
|
|
7
7
|
import { c as calculateOverallCoverage, p as parseLcov, b as analyzeFile } from '../analyzer.js';
|
|
8
8
|
|
|
9
|
+
const SCHEME = "brennpunkt";
|
|
10
|
+
function parseUri(uri) {
|
|
11
|
+
if (!uri.startsWith(`${SCHEME}://`)) {
|
|
12
|
+
throw new Error(`Invalid URI scheme: ${uri}. Expected ${SCHEME}://`);
|
|
13
|
+
}
|
|
14
|
+
const withoutScheme = uri.substring(`${SCHEME}://`.length);
|
|
15
|
+
const [pathPart, queryPart] = withoutScheme.split("?");
|
|
16
|
+
const segments = pathPart.split("/");
|
|
17
|
+
if (segments.length === 0 || !segments[0]) {
|
|
18
|
+
throw new Error(`Invalid URI: ${uri}. No resource type specified.`);
|
|
19
|
+
}
|
|
20
|
+
const resourceType = segments[0];
|
|
21
|
+
const params = parseQueryParams(queryPart);
|
|
22
|
+
switch (resourceType) {
|
|
23
|
+
case "coverage":
|
|
24
|
+
return parseCoverageUri(segments, params);
|
|
25
|
+
case "file":
|
|
26
|
+
return parseFileUri(segments, params);
|
|
27
|
+
case "priorities":
|
|
28
|
+
return parsePrioritiesUri(params);
|
|
29
|
+
case "config":
|
|
30
|
+
return parseConfigUri(segments, params);
|
|
31
|
+
case "quick-wins":
|
|
32
|
+
return parseQuickWinsUri(params);
|
|
33
|
+
default:
|
|
34
|
+
throw new Error(`Unknown resource type: ${resourceType}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function parseQueryParams(queryPart) {
|
|
38
|
+
if (!queryPart) return {};
|
|
39
|
+
const params = {};
|
|
40
|
+
const pairs = queryPart.split("&");
|
|
41
|
+
for (const pair of pairs) {
|
|
42
|
+
const [key, value] = pair.split("=");
|
|
43
|
+
if (key && value !== void 0) {
|
|
44
|
+
params[decodeURIComponent(key)] = decodeURIComponent(value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return params;
|
|
48
|
+
}
|
|
49
|
+
function parseCoverageUri(segments, params) {
|
|
50
|
+
const pathSegments = segments.slice(1);
|
|
51
|
+
let projectPath;
|
|
52
|
+
if (pathSegments.length === 0) {
|
|
53
|
+
throw new Error("Coverage URI requires project path");
|
|
54
|
+
}
|
|
55
|
+
if (pathSegments[0] === "") {
|
|
56
|
+
projectPath = "/" + pathSegments.slice(1).join("/");
|
|
57
|
+
} else {
|
|
58
|
+
projectPath = pathSegments.join("/");
|
|
59
|
+
}
|
|
60
|
+
if (!projectPath || projectPath === "/") {
|
|
61
|
+
throw new Error("Coverage URI requires project path");
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
scheme: SCHEME,
|
|
65
|
+
resourceType: "coverage",
|
|
66
|
+
projectPath: decodeURIComponent(projectPath),
|
|
67
|
+
params
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function parseFileUri(segments, params) {
|
|
71
|
+
if (segments.length < 3) {
|
|
72
|
+
throw new Error("File URI requires project path and file path");
|
|
73
|
+
}
|
|
74
|
+
const projectPath = decodeURIComponent(segments[1]);
|
|
75
|
+
const filePath = segments.slice(2).join("/");
|
|
76
|
+
return {
|
|
77
|
+
scheme: SCHEME,
|
|
78
|
+
resourceType: "file",
|
|
79
|
+
projectPath: decodeURIComponent(projectPath),
|
|
80
|
+
filePath: decodeURIComponent(filePath),
|
|
81
|
+
params
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function parsePrioritiesUri(params) {
|
|
85
|
+
const projectPath = params.project;
|
|
86
|
+
if (!projectPath) {
|
|
87
|
+
throw new Error("Priorities URI requires project parameter");
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
scheme: SCHEME,
|
|
91
|
+
resourceType: "priorities",
|
|
92
|
+
projectPath,
|
|
93
|
+
params
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function parseConfigUri(segments, params) {
|
|
97
|
+
const pathSegments = segments.slice(1);
|
|
98
|
+
let projectPath;
|
|
99
|
+
if (pathSegments.length === 0) {
|
|
100
|
+
throw new Error("Config URI requires project path");
|
|
101
|
+
}
|
|
102
|
+
if (pathSegments[0] === "") {
|
|
103
|
+
projectPath = "/" + pathSegments.slice(1).join("/");
|
|
104
|
+
} else {
|
|
105
|
+
projectPath = pathSegments.join("/");
|
|
106
|
+
}
|
|
107
|
+
if (!projectPath || projectPath === "/") {
|
|
108
|
+
throw new Error("Config URI requires project path");
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
scheme: SCHEME,
|
|
112
|
+
resourceType: "config",
|
|
113
|
+
projectPath: decodeURIComponent(projectPath),
|
|
114
|
+
params
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function parseQuickWinsUri(params) {
|
|
118
|
+
const projectPath = params.project;
|
|
119
|
+
if (!projectPath) {
|
|
120
|
+
throw new Error("Quick wins URI requires project parameter");
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
scheme: SCHEME,
|
|
124
|
+
resourceType: "quick-wins",
|
|
125
|
+
projectPath,
|
|
126
|
+
params
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function buildCoverageUri(projectPath) {
|
|
130
|
+
return `${SCHEME}://coverage/${encodeURIComponent(projectPath).replace(/%2F/g, "/")}`;
|
|
131
|
+
}
|
|
132
|
+
function buildFileUri(projectPath, filePath) {
|
|
133
|
+
const encodedProject = encodeURIComponent(projectPath);
|
|
134
|
+
const encodedFile = encodeURIComponent(filePath).replace(/%2F/g, "/");
|
|
135
|
+
return `${SCHEME}://file/${encodedProject}/${encodedFile}`;
|
|
136
|
+
}
|
|
137
|
+
function buildPrioritiesUri(projectPath, options) {
|
|
138
|
+
const params = new URLSearchParams();
|
|
139
|
+
params.set("project", projectPath);
|
|
140
|
+
if (options?.top) params.set("top", String(options.top));
|
|
141
|
+
if (options?.minLines) params.set("minLines", String(options.minLines));
|
|
142
|
+
return `${SCHEME}://priorities?${params.toString()}`;
|
|
143
|
+
}
|
|
144
|
+
function buildConfigUri(projectPath) {
|
|
145
|
+
return `${SCHEME}://config/${encodeURIComponent(projectPath).replace(/%2F/g, "/")}`;
|
|
146
|
+
}
|
|
147
|
+
function buildQuickWinsUri(projectPath, maxLines) {
|
|
148
|
+
const params = new URLSearchParams();
|
|
149
|
+
params.set("project", projectPath);
|
|
150
|
+
if (maxLines) params.set("maxLines", String(maxLines));
|
|
151
|
+
return `${SCHEME}://quick-wins?${params.toString()}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const resourceTemplates = [
|
|
155
|
+
{
|
|
156
|
+
uriTemplate: "brennpunkt://coverage/{projectPath}",
|
|
157
|
+
name: "Coverage Report",
|
|
158
|
+
description: "Full coverage data for a project",
|
|
159
|
+
mimeType: "application/json"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
uriTemplate: "brennpunkt://file/{projectPath}/{filePath}",
|
|
163
|
+
name: "File Coverage",
|
|
164
|
+
description: "Detailed coverage for a specific file",
|
|
165
|
+
mimeType: "application/json"
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
uriTemplate: "brennpunkt://priorities?project={projectPath}",
|
|
169
|
+
name: "Priority List",
|
|
170
|
+
description: "Files ranked by testing priority",
|
|
171
|
+
mimeType: "application/json"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
uriTemplate: "brennpunkt://config/{projectPath}",
|
|
175
|
+
name: "Project Configuration",
|
|
176
|
+
description: "Brennpunkt configuration for a project",
|
|
177
|
+
mimeType: "application/json"
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
uriTemplate: "brennpunkt://quick-wins?project={projectPath}",
|
|
181
|
+
name: "Quick Wins",
|
|
182
|
+
description: "Small files with high impact potential",
|
|
183
|
+
mimeType: "application/json"
|
|
184
|
+
}
|
|
185
|
+
];
|
|
186
|
+
async function handleListResources() {
|
|
187
|
+
return {
|
|
188
|
+
resources: [],
|
|
189
|
+
resourceTemplates
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
async function handleReadResource(uri) {
|
|
193
|
+
const parsed = parseUri(uri);
|
|
194
|
+
switch (parsed.resourceType) {
|
|
195
|
+
case "coverage":
|
|
196
|
+
return readCoverageResource(parsed.projectPath);
|
|
197
|
+
case "file":
|
|
198
|
+
return readFileResource(parsed.projectPath, parsed.filePath);
|
|
199
|
+
case "priorities":
|
|
200
|
+
return readPrioritiesResource(parsed.projectPath, parsed.params);
|
|
201
|
+
case "config":
|
|
202
|
+
return readConfigResource(parsed.projectPath);
|
|
203
|
+
case "quick-wins":
|
|
204
|
+
return readQuickWinsResource(parsed.projectPath, parsed.params);
|
|
205
|
+
default:
|
|
206
|
+
throw new Error(`Unknown resource type: ${parsed.resourceType}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function readCoverageResource(projectPath) {
|
|
210
|
+
const config = loadProjectConfig(projectPath);
|
|
211
|
+
const files = loadCoverage(projectPath, config);
|
|
212
|
+
const overall = calculateOverallCoverage(files);
|
|
213
|
+
const uri = buildCoverageUri(resolve(projectPath));
|
|
214
|
+
const data = {
|
|
215
|
+
projectPath: resolve(projectPath),
|
|
216
|
+
configUsed: existsSync(resolve(projectPath, "brennpunkt.yaml")) ? "brennpunkt.yaml" : "defaults",
|
|
217
|
+
overall: {
|
|
218
|
+
lines: overall.lines.coverage,
|
|
219
|
+
functions: overall.functions.coverage,
|
|
220
|
+
branches: overall.branches.coverage,
|
|
221
|
+
fileCount: overall.fileCount
|
|
222
|
+
},
|
|
223
|
+
files: files.map((f) => ({
|
|
224
|
+
file: f.file,
|
|
225
|
+
lines: {
|
|
226
|
+
hit: f.linesHit,
|
|
227
|
+
found: f.linesFound,
|
|
228
|
+
coverage: f.linesFound > 0 ? f.linesHit / f.linesFound * 100 : 100
|
|
229
|
+
},
|
|
230
|
+
functions: {
|
|
231
|
+
hit: f.functionsHit,
|
|
232
|
+
found: f.functionsFound,
|
|
233
|
+
coverage: f.functionsFound > 0 ? f.functionsHit / f.functionsFound * 100 : 100
|
|
234
|
+
},
|
|
235
|
+
branches: {
|
|
236
|
+
hit: f.branchesHit,
|
|
237
|
+
found: f.branchesFound,
|
|
238
|
+
coverage: f.branchesFound > 0 ? f.branchesHit / f.branchesFound * 100 : 100
|
|
239
|
+
}
|
|
240
|
+
}))
|
|
241
|
+
};
|
|
242
|
+
return {
|
|
243
|
+
uri,
|
|
244
|
+
mimeType: "application/json",
|
|
245
|
+
text: JSON.stringify(data, null, 2)
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
async function readFileResource(projectPath, filePath) {
|
|
249
|
+
const config = loadProjectConfig(projectPath);
|
|
250
|
+
const files = loadCoverage(projectPath, config);
|
|
251
|
+
const file = files.find(
|
|
252
|
+
(f) => f.file === filePath || f.file.endsWith(filePath) || filePath.endsWith(f.file)
|
|
253
|
+
);
|
|
254
|
+
if (!file) {
|
|
255
|
+
throw new Error(`File not found in coverage data: ${filePath}`);
|
|
256
|
+
}
|
|
257
|
+
const lineCov = file.linesFound > 0 ? file.linesHit / file.linesFound * 100 : 100;
|
|
258
|
+
const funcCov = file.functionsFound > 0 ? file.functionsHit / file.functionsFound * 100 : 100;
|
|
259
|
+
const branchCov = file.branchesFound > 0 ? file.branchesHit / file.branchesFound * 100 : 100;
|
|
260
|
+
const uri = buildFileUri(resolve(projectPath), filePath);
|
|
261
|
+
const data = {
|
|
262
|
+
projectPath: resolve(projectPath),
|
|
263
|
+
configUsed: existsSync(resolve(projectPath, "brennpunkt.yaml")) ? "brennpunkt.yaml" : "defaults",
|
|
264
|
+
file: file.file,
|
|
265
|
+
coverage: {
|
|
266
|
+
lines: {
|
|
267
|
+
covered: file.linesHit,
|
|
268
|
+
total: file.linesFound,
|
|
269
|
+
percentage: Math.round(lineCov * 10) / 10
|
|
270
|
+
},
|
|
271
|
+
functions: {
|
|
272
|
+
covered: file.functionsHit,
|
|
273
|
+
total: file.functionsFound,
|
|
274
|
+
percentage: Math.round(funcCov * 10) / 10
|
|
275
|
+
},
|
|
276
|
+
branches: {
|
|
277
|
+
covered: file.branchesHit,
|
|
278
|
+
total: file.branchesFound,
|
|
279
|
+
percentage: Math.round(branchCov * 10) / 10
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
uncovered: {
|
|
283
|
+
lines: file.linesFound - file.linesHit,
|
|
284
|
+
functions: file.functionsFound - file.functionsHit,
|
|
285
|
+
branches: file.branchesFound - file.branchesHit
|
|
286
|
+
},
|
|
287
|
+
suggestedFocus: generateSuggestedFocus(file)
|
|
288
|
+
};
|
|
289
|
+
return {
|
|
290
|
+
uri,
|
|
291
|
+
mimeType: "application/json",
|
|
292
|
+
text: JSON.stringify(data, null, 2)
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
async function readPrioritiesResource(projectPath, params) {
|
|
296
|
+
const config = loadProjectConfig(projectPath);
|
|
297
|
+
const files = loadCoverage(projectPath, config);
|
|
298
|
+
const top = params.top ? parseInt(params.top, 10) : config.top ?? 5;
|
|
299
|
+
const minLines = params.minLines ? parseInt(params.minLines, 10) : config.minLines ?? 10;
|
|
300
|
+
const weights = config.weights ?? DEFAULT_WEIGHTS;
|
|
301
|
+
const priorities = getPriorities(files, top, minLines, weights);
|
|
302
|
+
const overall = calculateOverallCoverage(files);
|
|
303
|
+
const uri = buildPrioritiesUri(resolve(projectPath), { top, minLines });
|
|
304
|
+
const data = {
|
|
305
|
+
projectPath: resolve(projectPath),
|
|
306
|
+
configUsed: existsSync(resolve(projectPath, "brennpunkt.yaml")) ? "brennpunkt.yaml" : "defaults",
|
|
307
|
+
overall: {
|
|
308
|
+
lines: overall.lines.coverage,
|
|
309
|
+
functions: overall.functions.coverage,
|
|
310
|
+
branches: overall.branches.coverage,
|
|
311
|
+
fileCount: overall.fileCount
|
|
312
|
+
},
|
|
313
|
+
priorities
|
|
314
|
+
};
|
|
315
|
+
return {
|
|
316
|
+
uri,
|
|
317
|
+
mimeType: "application/json",
|
|
318
|
+
text: JSON.stringify(data, null, 2)
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
async function readConfigResource(projectPath) {
|
|
322
|
+
const config = loadProjectConfig(projectPath);
|
|
323
|
+
const configPath = resolve(projectPath, "brennpunkt.yaml");
|
|
324
|
+
const hasConfig = existsSync(configPath);
|
|
325
|
+
const uri = buildConfigUri(resolve(projectPath));
|
|
326
|
+
const data = {
|
|
327
|
+
projectPath: resolve(projectPath),
|
|
328
|
+
configPath,
|
|
329
|
+
hasConfig,
|
|
330
|
+
config: {
|
|
331
|
+
coveragePath: config.coveragePath ?? "auto-detect",
|
|
332
|
+
weights: config.weights ?? DEFAULT_WEIGHTS,
|
|
333
|
+
minLines: config.minLines ?? 10,
|
|
334
|
+
top: config.top ?? 5
|
|
335
|
+
},
|
|
336
|
+
source: hasConfig ? "brennpunkt.yaml" : "defaults"
|
|
337
|
+
};
|
|
338
|
+
return {
|
|
339
|
+
uri,
|
|
340
|
+
mimeType: "application/json",
|
|
341
|
+
text: JSON.stringify(data, null, 2)
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
async function readQuickWinsResource(projectPath, params) {
|
|
345
|
+
const config = loadProjectConfig(projectPath);
|
|
346
|
+
const files = loadCoverage(projectPath, config);
|
|
347
|
+
const overall = calculateOverallCoverage(files);
|
|
348
|
+
const minLines = config.minLines ?? 10;
|
|
349
|
+
const maxLines = params.maxLines ? parseInt(params.maxLines, 10) : 100;
|
|
350
|
+
const quickWins = files.filter((f) => f.linesFound >= minLines && f.linesFound <= maxLines).map((f) => {
|
|
351
|
+
const uncoveredLines = f.linesFound - f.linesHit;
|
|
352
|
+
const potentialImpact = overall.lines.found > 0 ? uncoveredLines / overall.lines.found * 100 : 0;
|
|
353
|
+
const currentCoverage = f.linesFound > 0 ? f.linesHit / f.linesFound * 100 : 100;
|
|
354
|
+
return {
|
|
355
|
+
file: f.file,
|
|
356
|
+
linesTotal: f.linesFound,
|
|
357
|
+
uncoveredLines,
|
|
358
|
+
currentCoverage: Math.round(currentCoverage * 10) / 10,
|
|
359
|
+
potentialImpact: Math.round(potentialImpact * 100) / 100
|
|
360
|
+
};
|
|
361
|
+
}).filter((f) => f.uncoveredLines > 0).sort((a, b) => b.potentialImpact - a.potentialImpact).slice(0, 10);
|
|
362
|
+
const uri = buildQuickWinsUri(resolve(projectPath), maxLines);
|
|
363
|
+
const data = {
|
|
364
|
+
projectPath: resolve(projectPath),
|
|
365
|
+
configUsed: existsSync(resolve(projectPath, "brennpunkt.yaml")) ? "brennpunkt.yaml" : "defaults",
|
|
366
|
+
criteria: {
|
|
367
|
+
minLines,
|
|
368
|
+
maxLines
|
|
369
|
+
},
|
|
370
|
+
overall: {
|
|
371
|
+
lines: overall.lines.coverage
|
|
372
|
+
},
|
|
373
|
+
quickWins
|
|
374
|
+
};
|
|
375
|
+
return {
|
|
376
|
+
uri,
|
|
377
|
+
mimeType: "application/json",
|
|
378
|
+
text: JSON.stringify(data, null, 2)
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const prompts = [
|
|
383
|
+
{
|
|
384
|
+
name: "improve_coverage",
|
|
385
|
+
description: "Complete workflow to improve test coverage to a target percentage. Analyzes current state, identifies gaps, estimates effort, and provides actionable plan.",
|
|
386
|
+
arguments: [
|
|
387
|
+
{
|
|
388
|
+
name: "projectPath",
|
|
389
|
+
description: "Absolute path to the project",
|
|
390
|
+
required: true
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
name: "targetPercentage",
|
|
394
|
+
description: "Target coverage percentage (default: 90)",
|
|
395
|
+
required: false
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
name: "focusMetric",
|
|
399
|
+
description: "Metric to focus on: lines, branches, or functions (default: lines)",
|
|
400
|
+
required: false
|
|
401
|
+
}
|
|
402
|
+
]
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
name: "analyze_gaps",
|
|
406
|
+
description: "Understand why coverage is below target. Identifies patterns, problematic modules, and root causes.",
|
|
407
|
+
arguments: [
|
|
408
|
+
{
|
|
409
|
+
name: "projectPath",
|
|
410
|
+
description: "Absolute path to the project",
|
|
411
|
+
required: true
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
name: "targetPercentage",
|
|
415
|
+
description: "Target coverage to compare against (default: 90)",
|
|
416
|
+
required: false
|
|
417
|
+
}
|
|
418
|
+
]
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
name: "quick_wins_workflow",
|
|
422
|
+
description: "Find fast paths to coverage improvement. Identifies small files with high impact, estimates effort and total improvement.",
|
|
423
|
+
arguments: [
|
|
424
|
+
{
|
|
425
|
+
name: "projectPath",
|
|
426
|
+
description: "Absolute path to the project",
|
|
427
|
+
required: true
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
name: "timeConstraint",
|
|
431
|
+
description: "Time available: quick (5 files), moderate (10 files), thorough (20 files)",
|
|
432
|
+
required: false
|
|
433
|
+
}
|
|
434
|
+
]
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
name: "coverage_review",
|
|
438
|
+
description: "Code review focused on coverage gaps. Analyzes specific files, identifies untested functions/branches, suggests test cases.",
|
|
439
|
+
arguments: [
|
|
440
|
+
{
|
|
441
|
+
name: "projectPath",
|
|
442
|
+
description: "Absolute path to the project",
|
|
443
|
+
required: true
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
name: "filePattern",
|
|
447
|
+
description: "Filter to specific files (optional)",
|
|
448
|
+
required: false
|
|
449
|
+
}
|
|
450
|
+
]
|
|
451
|
+
}
|
|
452
|
+
];
|
|
453
|
+
async function handleListPrompts() {
|
|
454
|
+
return { prompts };
|
|
455
|
+
}
|
|
456
|
+
async function handleGetPrompt(name, args) {
|
|
457
|
+
const prompt = prompts.find((p) => p.name === name);
|
|
458
|
+
if (!prompt) {
|
|
459
|
+
throw new Error(`Unknown prompt: ${name}`);
|
|
460
|
+
}
|
|
461
|
+
for (const arg of prompt.arguments || []) {
|
|
462
|
+
if (arg.required && !args[arg.name]) {
|
|
463
|
+
throw new Error(`Missing required argument: ${arg.name}`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
switch (name) {
|
|
467
|
+
case "improve_coverage":
|
|
468
|
+
return generateImproveCoveragePrompt(args);
|
|
469
|
+
case "analyze_gaps":
|
|
470
|
+
return generateAnalyzeGapsPrompt(args);
|
|
471
|
+
case "quick_wins_workflow":
|
|
472
|
+
return generateQuickWinsWorkflowPrompt(args);
|
|
473
|
+
case "coverage_review":
|
|
474
|
+
return generateCoverageReviewPrompt(args);
|
|
475
|
+
default:
|
|
476
|
+
throw new Error(`Prompt handler not implemented: ${name}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
async function generateImproveCoveragePrompt(args) {
|
|
480
|
+
const projectPath = args.projectPath;
|
|
481
|
+
const target = parseFloat(args.targetPercentage || "90");
|
|
482
|
+
const metric = args.focusMetric || "lines";
|
|
483
|
+
const messages = [];
|
|
484
|
+
messages.push({
|
|
485
|
+
role: "user",
|
|
486
|
+
content: {
|
|
487
|
+
type: "text",
|
|
488
|
+
text: `I want to improve test coverage for ${projectPath} to ${target}% ${metric} coverage.`
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
let assistantText = `I'll help you improve test coverage to ${target}%.
|
|
492
|
+
|
|
493
|
+
`;
|
|
494
|
+
assistantText += `**Workflow:**
|
|
495
|
+
`;
|
|
496
|
+
assistantText += `1. Check current coverage: \`brennpunkt_coverage_summary\` with projectPath="${projectPath}"
|
|
497
|
+
`;
|
|
498
|
+
assistantText += `2. Get priorities: \`brennpunkt_get_priorities\` to identify top files
|
|
499
|
+
`;
|
|
500
|
+
assistantText += `3. Estimate impact: \`brennpunkt_estimate_impact\` to verify we'll hit ${target}%
|
|
501
|
+
`;
|
|
502
|
+
assistantText += `4. Write tests for highest priority files
|
|
503
|
+
|
|
504
|
+
`;
|
|
505
|
+
assistantText += `**Resources available:**
|
|
506
|
+
`;
|
|
507
|
+
assistantText += `- Coverage data: ${buildCoverageUri(projectPath)}
|
|
508
|
+
`;
|
|
509
|
+
assistantText += `- Priorities: ${buildPrioritiesUri(projectPath, { top: 5 })}
|
|
510
|
+
|
|
511
|
+
`;
|
|
512
|
+
assistantText += `Let me start by checking your current coverage...`;
|
|
513
|
+
messages.push({
|
|
514
|
+
role: "assistant",
|
|
515
|
+
content: {
|
|
516
|
+
type: "text",
|
|
517
|
+
text: assistantText
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
return { messages };
|
|
521
|
+
}
|
|
522
|
+
async function generateAnalyzeGapsPrompt(args) {
|
|
523
|
+
const projectPath = args.projectPath;
|
|
524
|
+
const target = parseFloat(args.targetPercentage || "90");
|
|
525
|
+
const messages = [];
|
|
526
|
+
messages.push({
|
|
527
|
+
role: "user",
|
|
528
|
+
content: {
|
|
529
|
+
type: "text",
|
|
530
|
+
text: `Analyze coverage gaps in ${projectPath}. Why is coverage below ${target}%?`
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
let assistantText = `I'll analyze coverage gaps to understand the patterns.
|
|
534
|
+
|
|
535
|
+
`;
|
|
536
|
+
assistantText += `**Analysis Plan:**
|
|
537
|
+
`;
|
|
538
|
+
assistantText += `1. Get current coverage summary: \`brennpunkt_coverage_summary\`
|
|
539
|
+
`;
|
|
540
|
+
assistantText += `2. Examine top priority files: \`brennpunkt_get_priorities\` with top=10
|
|
541
|
+
`;
|
|
542
|
+
assistantText += `3. Look for patterns:
|
|
543
|
+
`;
|
|
544
|
+
assistantText += ` - Are gaps in specific modules?
|
|
545
|
+
`;
|
|
546
|
+
assistantText += ` - Common issue (branches, functions, lines)?
|
|
547
|
+
`;
|
|
548
|
+
assistantText += ` - Error handling missing?
|
|
549
|
+
`;
|
|
550
|
+
assistantText += ` - Edge cases untested?
|
|
551
|
+
|
|
552
|
+
`;
|
|
553
|
+
assistantText += `**Resources:**
|
|
554
|
+
`;
|
|
555
|
+
assistantText += `- Full coverage: ${buildCoverageUri(projectPath)}
|
|
556
|
+
`;
|
|
557
|
+
assistantText += `- Priority analysis: ${buildPrioritiesUri(projectPath, { top: 10 })}
|
|
558
|
+
|
|
559
|
+
`;
|
|
560
|
+
assistantText += `Let me examine the coverage data...`;
|
|
561
|
+
messages.push({
|
|
562
|
+
role: "assistant",
|
|
563
|
+
content: {
|
|
564
|
+
type: "text",
|
|
565
|
+
text: assistantText
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
return { messages };
|
|
569
|
+
}
|
|
570
|
+
async function generateQuickWinsWorkflowPrompt(args) {
|
|
571
|
+
const projectPath = args.projectPath;
|
|
572
|
+
const constraint = args.timeConstraint || "moderate";
|
|
573
|
+
const fileCount = constraint === "quick" ? 5 : constraint === "thorough" ? 20 : 10;
|
|
574
|
+
const messages = [];
|
|
575
|
+
messages.push({
|
|
576
|
+
role: "user",
|
|
577
|
+
content: {
|
|
578
|
+
type: "text",
|
|
579
|
+
text: `Find quick wins to improve coverage in ${projectPath}. I have ${constraint} time available.`
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
let assistantText = `I'll find ${fileCount} quick wins - small files with high impact.
|
|
583
|
+
|
|
584
|
+
`;
|
|
585
|
+
assistantText += `**Strategy:**
|
|
586
|
+
`;
|
|
587
|
+
assistantText += `1. Get quick wins: Use resource ${buildQuickWinsUri(projectPath, 100)}
|
|
588
|
+
`;
|
|
589
|
+
assistantText += `2. Estimate total impact: \`brennpunkt_estimate_impact\` with identified files
|
|
590
|
+
`;
|
|
591
|
+
assistantText += `3. Present in priority order with effort estimates
|
|
592
|
+
`;
|
|
593
|
+
assistantText += `4. Suggest test approaches for each
|
|
594
|
+
|
|
595
|
+
`;
|
|
596
|
+
assistantText += `**Criteria:**
|
|
597
|
+
`;
|
|
598
|
+
assistantText += `- Small files (<100 lines)
|
|
599
|
+
`;
|
|
600
|
+
assistantText += `- High impact potential
|
|
601
|
+
`;
|
|
602
|
+
assistantText += `- Currently have coverage gaps
|
|
603
|
+
|
|
604
|
+
`;
|
|
605
|
+
assistantText += `Let me identify the quick wins...`;
|
|
606
|
+
messages.push({
|
|
607
|
+
role: "assistant",
|
|
608
|
+
content: {
|
|
609
|
+
type: "text",
|
|
610
|
+
text: assistantText
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
return { messages };
|
|
614
|
+
}
|
|
615
|
+
async function generateCoverageReviewPrompt(args) {
|
|
616
|
+
const projectPath = args.projectPath;
|
|
617
|
+
const pattern = args.filePattern || "highest priority files";
|
|
618
|
+
const messages = [];
|
|
619
|
+
messages.push({
|
|
620
|
+
role: "user",
|
|
621
|
+
content: {
|
|
622
|
+
type: "text",
|
|
623
|
+
text: `Review coverage for ${pattern} in ${projectPath}. What tests should I write?`
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
let assistantText = `I'll perform a detailed coverage review.
|
|
627
|
+
|
|
628
|
+
`;
|
|
629
|
+
assistantText += `**Review Process:**
|
|
630
|
+
`;
|
|
631
|
+
assistantText += `1. Get priorities: \`brennpunkt_get_priorities\` to identify files
|
|
632
|
+
`;
|
|
633
|
+
assistantText += `2. For each file:
|
|
634
|
+
`;
|
|
635
|
+
assistantText += ` - Get details: \`brennpunkt_get_file_coverage\`
|
|
636
|
+
`;
|
|
637
|
+
assistantText += ` - Identify untested functions/branches
|
|
638
|
+
`;
|
|
639
|
+
assistantText += ` - Suggest specific test cases
|
|
640
|
+
`;
|
|
641
|
+
assistantText += `3. Prioritize suggestions by impact
|
|
642
|
+
|
|
643
|
+
`;
|
|
644
|
+
assistantText += `**Resources:**
|
|
645
|
+
`;
|
|
646
|
+
assistantText += `- Priorities: ${buildPrioritiesUri(projectPath, { top: 5 })}
|
|
647
|
+
|
|
648
|
+
`;
|
|
649
|
+
assistantText += `Let me start the review...`;
|
|
650
|
+
messages.push({
|
|
651
|
+
role: "assistant",
|
|
652
|
+
content: {
|
|
653
|
+
type: "text",
|
|
654
|
+
text: assistantText
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
return { messages };
|
|
658
|
+
}
|
|
659
|
+
|
|
9
660
|
const COVERAGE_SEARCH_PATHS = [
|
|
10
661
|
"coverage/lcov.info",
|
|
11
662
|
".coverage/lcov.info",
|
|
@@ -430,13 +1081,20 @@ async function main() {
|
|
|
430
1081
|
const server = new Server(
|
|
431
1082
|
{
|
|
432
1083
|
name: "brennpunkt",
|
|
433
|
-
version: "0.0
|
|
1084
|
+
version: "0.1.0",
|
|
434
1085
|
// Server-level description for AI tools discovering this MCP server
|
|
435
1086
|
description: "Test coverage priority analyzer. Reads lcov.info coverage data (produced by Jest, Vitest, Mocha, c8, NYC, Karma, Playwright, or any lcov-compatible tool) and ranks files by testing priority. Use this to identify WHERE to focus testing efforts for maximum coverage impact WITHOUT running tests. Provides actionable insights: priority scores, coverage gaps, and specific suggestions for each file."
|
|
436
1087
|
},
|
|
437
1088
|
{
|
|
438
1089
|
capabilities: {
|
|
439
|
-
tools: {}
|
|
1090
|
+
tools: {},
|
|
1091
|
+
resources: {
|
|
1092
|
+
subscribe: false,
|
|
1093
|
+
listChanged: false
|
|
1094
|
+
},
|
|
1095
|
+
prompts: {
|
|
1096
|
+
listChanged: false
|
|
1097
|
+
}
|
|
440
1098
|
}
|
|
441
1099
|
}
|
|
442
1100
|
);
|
|
@@ -477,8 +1135,35 @@ async function main() {
|
|
|
477
1135
|
};
|
|
478
1136
|
}
|
|
479
1137
|
});
|
|
1138
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
1139
|
+
return handleListResources();
|
|
1140
|
+
});
|
|
1141
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
1142
|
+
const { uri } = request.params;
|
|
1143
|
+
try {
|
|
1144
|
+
const contents = await handleReadResource(uri);
|
|
1145
|
+
return { contents: [contents] };
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1148
|
+
throw new Error(`Failed to read resource ${uri}: ${message}`);
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
1152
|
+
return handleListPrompts();
|
|
1153
|
+
});
|
|
1154
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
1155
|
+
const { name, arguments: args } = request.params;
|
|
1156
|
+
try {
|
|
1157
|
+
return handleGetPrompt(name, args || {});
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1160
|
+
throw new Error(`Failed to get prompt ${name}: ${message}`);
|
|
1161
|
+
}
|
|
1162
|
+
});
|
|
480
1163
|
const transport = new StdioServerTransport();
|
|
481
1164
|
await server.connect(transport);
|
|
482
1165
|
}
|
|
483
1166
|
main().catch(console.error);
|
|
1167
|
+
|
|
1168
|
+
export { DEFAULT_CONFIG, DEFAULT_WEIGHTS, generateSuggestedFocus, getPriorities, loadCoverage, loadProjectConfig, validateProjectPath };
|
|
484
1169
|
//# sourceMappingURL=server.js.map
|