preflight-mcp 0.1.2 → 0.1.3
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 +35 -142
- package/README.zh-CN.md +141 -124
- package/dist/ast/treeSitter.js +588 -0
- package/dist/bundle/analysis.js +47 -0
- package/dist/bundle/context7.js +65 -36
- package/dist/bundle/facts.js +829 -0
- package/dist/bundle/githubArchive.js +49 -28
- package/dist/bundle/overview.js +226 -48
- package/dist/bundle/service.js +27 -126
- package/dist/config.js +29 -3
- package/dist/context7/client.js +5 -2
- package/dist/evidence/dependencyGraph.js +826 -0
- package/dist/http/server.js +109 -0
- package/dist/search/sqliteFts.js +150 -10
- package/dist/server.js +84 -295
- package/dist/trace/service.js +108 -0
- package/dist/trace/store.js +170 -0
- package/package.json +4 -2
- package/dist/bundle/deepwiki.js +0 -206
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { URL } from 'node:url';
|
|
3
|
+
import { logger } from '../logging/logger.js';
|
|
4
|
+
import { wrapPreflightError } from '../mcp/errorKinds.js';
|
|
5
|
+
import { generateDependencyGraph } from '../evidence/dependencyGraph.js';
|
|
6
|
+
import { traceQuery, traceUpsert } from '../trace/service.js';
|
|
7
|
+
function sendJson(res, status, body) {
|
|
8
|
+
const text = JSON.stringify(body, null, 2);
|
|
9
|
+
res.statusCode = status;
|
|
10
|
+
res.setHeader('content-type', 'application/json; charset=utf-8');
|
|
11
|
+
res.setHeader('cache-control', 'no-store');
|
|
12
|
+
res.end(text);
|
|
13
|
+
}
|
|
14
|
+
async function readJsonBody(req, maxBytes) {
|
|
15
|
+
const chunks = [];
|
|
16
|
+
let total = 0;
|
|
17
|
+
for await (const chunk of req) {
|
|
18
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
19
|
+
total += buf.length;
|
|
20
|
+
if (total > maxBytes) {
|
|
21
|
+
throw new Error(`Request body too large (>${maxBytes} bytes)`);
|
|
22
|
+
}
|
|
23
|
+
chunks.push(buf);
|
|
24
|
+
}
|
|
25
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
26
|
+
if (!raw.trim())
|
|
27
|
+
return {};
|
|
28
|
+
return JSON.parse(raw);
|
|
29
|
+
}
|
|
30
|
+
export function startHttpServer(cfg) {
|
|
31
|
+
if (!cfg.httpEnabled) {
|
|
32
|
+
logger.info('REST API disabled (PREFLIGHT_HTTP_ENABLED=false)');
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const server = http.createServer(async (req, res) => {
|
|
36
|
+
try {
|
|
37
|
+
const method = (req.method ?? 'GET').toUpperCase();
|
|
38
|
+
const url = new URL(req.url ?? '/', `http://${cfg.httpHost}:${cfg.httpPort}`);
|
|
39
|
+
const pathname = url.pathname;
|
|
40
|
+
// Basic CORS (local development convenience). Keep permissive but local-only by default host.
|
|
41
|
+
res.setHeader('access-control-allow-origin', '*');
|
|
42
|
+
res.setHeader('access-control-allow-headers', 'content-type');
|
|
43
|
+
res.setHeader('access-control-allow-methods', 'GET,POST,OPTIONS');
|
|
44
|
+
if (method === 'OPTIONS') {
|
|
45
|
+
res.statusCode = 204;
|
|
46
|
+
res.end();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (method === 'GET' && pathname === '/health') {
|
|
50
|
+
sendJson(res, 200, { ok: true, name: 'preflight-mcp', time: new Date().toISOString() });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (method === 'POST' && pathname === '/api/v1/evidence/dependency-graph') {
|
|
54
|
+
const body = await readJsonBody(req, 2_000_000);
|
|
55
|
+
const out = await generateDependencyGraph(cfg, body);
|
|
56
|
+
sendJson(res, 200, out);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (method === 'POST' && pathname === '/api/v1/trace/upsert') {
|
|
60
|
+
const body = await readJsonBody(req, 2_000_000);
|
|
61
|
+
const out = await traceUpsert(cfg, body);
|
|
62
|
+
sendJson(res, 200, out);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (method === 'GET' && pathname === '/api/v1/trace') {
|
|
66
|
+
const query = Object.fromEntries(url.searchParams.entries());
|
|
67
|
+
// Convert known numeric params
|
|
68
|
+
const body = { ...query };
|
|
69
|
+
if (typeof query.limit === 'string')
|
|
70
|
+
body.limit = Number(query.limit);
|
|
71
|
+
if (typeof query.timeBudgetMs === 'string')
|
|
72
|
+
body.timeBudgetMs = Number(query.timeBudgetMs);
|
|
73
|
+
if (typeof query.maxBundles === 'string')
|
|
74
|
+
body.maxBundles = Number(query.maxBundles);
|
|
75
|
+
const out = await traceQuery(cfg, body);
|
|
76
|
+
sendJson(res, 200, out);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
sendJson(res, 404, { error: { message: `Not found: ${method} ${pathname}` } });
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
const wrapped = wrapPreflightError(err);
|
|
83
|
+
sendJson(res, 400, { error: { message: wrapped.message } });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
server.on('error', (err) => {
|
|
87
|
+
// Non-fatal: MCP should still work.
|
|
88
|
+
logger.warn(`REST server error: ${err instanceof Error ? err.message : String(err)}`);
|
|
89
|
+
});
|
|
90
|
+
try {
|
|
91
|
+
server.listen(cfg.httpPort, cfg.httpHost, () => {
|
|
92
|
+
logger.info(`REST API listening on http://${cfg.httpHost}:${cfg.httpPort}`);
|
|
93
|
+
});
|
|
94
|
+
// Allow process to exit if stdio transport closes.
|
|
95
|
+
server.unref();
|
|
96
|
+
return {
|
|
97
|
+
host: cfg.httpHost,
|
|
98
|
+
port: cfg.httpPort,
|
|
99
|
+
close: () => new Promise((resolve) => {
|
|
100
|
+
server.close(() => resolve());
|
|
101
|
+
}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
// Binding failures should not crash MCP.
|
|
106
|
+
logger.warn(`Failed to start REST API server: ${err instanceof Error ? err.message : String(err)}`);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
package/dist/search/sqliteFts.js
CHANGED
|
@@ -278,6 +278,118 @@ export async function rebuildIndex(dbPathOrFiles, filesOrDbPath, opts) {
|
|
|
278
278
|
db.close();
|
|
279
279
|
}
|
|
280
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* Extract code context for a search hit.
|
|
283
|
+
* Finds the surrounding function/class definition and surrounding lines.
|
|
284
|
+
*/
|
|
285
|
+
function extractContext(fileContent, hitLineNo) {
|
|
286
|
+
const lines = fileContent.split('\n');
|
|
287
|
+
const hitIndex = hitLineNo - 1; // Convert 1-based to 0-based
|
|
288
|
+
if (hitIndex < 0 || hitIndex >= lines.length) {
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
// Extract surrounding lines (±3 lines)
|
|
292
|
+
const surroundStart = Math.max(0, hitIndex - 3);
|
|
293
|
+
const surroundEnd = Math.min(lines.length - 1, hitIndex + 3);
|
|
294
|
+
const surroundingLines = lines.slice(surroundStart, surroundEnd + 1);
|
|
295
|
+
// Find function/class definition by scanning upwards (max 50 lines)
|
|
296
|
+
let functionName;
|
|
297
|
+
let className;
|
|
298
|
+
let startLine = hitLineNo;
|
|
299
|
+
let endLine = hitLineNo;
|
|
300
|
+
const scanStartIndex = Math.max(0, hitIndex - 50);
|
|
301
|
+
// Patterns for TypeScript/JavaScript/Python/Go functions and classes
|
|
302
|
+
const functionPatterns = [
|
|
303
|
+
// TypeScript/JavaScript
|
|
304
|
+
/^\s*(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][\w$]*)\s*\(/,
|
|
305
|
+
/^\s*(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][\w$]*)\s*=\s*(?:async\s+)?\(/,
|
|
306
|
+
/^\s*(?:async\s+)?([a-zA-Z_$][\w$]*)\s*\([^)]*\)\s*\{/,
|
|
307
|
+
/^\s*([a-zA-Z_$][\w$]*)\s*:\s*(?:async\s+)?function\s*\(/,
|
|
308
|
+
// Python
|
|
309
|
+
/^\s*(?:async\s+)?def\s+([a-zA-Z_][\w]*)\s*\(/,
|
|
310
|
+
// Go
|
|
311
|
+
/^\s*func\s+(?:\([^)]*\)\s*)?([a-zA-Z_][\w]*)\s*\(/,
|
|
312
|
+
];
|
|
313
|
+
const classPatterns = [
|
|
314
|
+
// TypeScript/JavaScript
|
|
315
|
+
/^\s*(?:export\s+)?(?:abstract\s+)?class\s+([a-zA-Z_$][\w$]*)/,
|
|
316
|
+
/^\s*(?:export\s+)?interface\s+([a-zA-Z_$][\w$]*)/,
|
|
317
|
+
/^\s*(?:export\s+)?type\s+([a-zA-Z_$][\w$]*)\s*=/,
|
|
318
|
+
// Python
|
|
319
|
+
/^\s*class\s+([a-zA-Z_][\w]*)\s*[:(]/,
|
|
320
|
+
// Go
|
|
321
|
+
/^\s*type\s+([a-zA-Z_][\w]*)\s+struct/,
|
|
322
|
+
];
|
|
323
|
+
// Scan upward to find function or class definition
|
|
324
|
+
for (let i = hitIndex; i >= scanStartIndex; i--) {
|
|
325
|
+
const line = lines[i] ?? '';
|
|
326
|
+
// Try to match function patterns
|
|
327
|
+
if (!functionName) {
|
|
328
|
+
for (const pattern of functionPatterns) {
|
|
329
|
+
const match = line.match(pattern);
|
|
330
|
+
if (match?.[1]) {
|
|
331
|
+
functionName = match[1];
|
|
332
|
+
startLine = i + 1; // Convert to 1-based
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// Try to match class patterns (only if we haven't found function yet)
|
|
338
|
+
if (!className) {
|
|
339
|
+
for (const pattern of classPatterns) {
|
|
340
|
+
const match = line.match(pattern);
|
|
341
|
+
if (match?.[1]) {
|
|
342
|
+
className = match[1];
|
|
343
|
+
if (!functionName) {
|
|
344
|
+
startLine = i + 1;
|
|
345
|
+
}
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// If we found function name, stop scanning
|
|
351
|
+
if (functionName) {
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// Find end of the function/block by scanning downward for closing brace
|
|
356
|
+
// Simple bracket matching (stops at first balanced closing brace)
|
|
357
|
+
if (functionName || className) {
|
|
358
|
+
let braceCount = 0;
|
|
359
|
+
let foundOpenBrace = false;
|
|
360
|
+
for (let i = startLine - 1; i < lines.length && i < hitIndex + 100; i++) {
|
|
361
|
+
const line = lines[i] ?? '';
|
|
362
|
+
for (const char of line) {
|
|
363
|
+
if (char === '{') {
|
|
364
|
+
braceCount++;
|
|
365
|
+
foundOpenBrace = true;
|
|
366
|
+
}
|
|
367
|
+
else if (char === '}') {
|
|
368
|
+
braceCount--;
|
|
369
|
+
if (foundOpenBrace && braceCount === 0) {
|
|
370
|
+
endLine = i + 1;
|
|
371
|
+
return {
|
|
372
|
+
functionName,
|
|
373
|
+
className,
|
|
374
|
+
startLine,
|
|
375
|
+
endLine,
|
|
376
|
+
surroundingLines,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// If we didn't find closing brace, estimate end line
|
|
383
|
+
endLine = Math.min(lines.length, startLine + 50);
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
functionName,
|
|
387
|
+
className,
|
|
388
|
+
startLine,
|
|
389
|
+
endLine,
|
|
390
|
+
surroundingLines,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
281
393
|
function tokenizeForSafeQuery(input) {
|
|
282
394
|
const s = input.trim();
|
|
283
395
|
if (!s)
|
|
@@ -308,7 +420,7 @@ export function buildFtsQuery(input) {
|
|
|
308
420
|
// Quote each token to keep syntax safe.
|
|
309
421
|
return tokens.map((t) => `"${t.replaceAll('"', '""')}"`).join(' OR ');
|
|
310
422
|
}
|
|
311
|
-
export function searchIndex(dbPath, query, scope, limit) {
|
|
423
|
+
export function searchIndex(dbPath, query, scope, limit, bundleRoot) {
|
|
312
424
|
const db = new Database(dbPath, { readonly: true });
|
|
313
425
|
try {
|
|
314
426
|
const ftsQuery = buildFtsQuery(query);
|
|
@@ -326,13 +438,41 @@ export function searchIndex(dbPath, query, scope, limit) {
|
|
|
326
438
|
LIMIT ?
|
|
327
439
|
`);
|
|
328
440
|
const rows = stmt.all(ftsQuery, limit);
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
441
|
+
// Cache for file contents to avoid re-reading same files
|
|
442
|
+
const fileCache = new Map();
|
|
443
|
+
return rows.map((r) => {
|
|
444
|
+
const hit = {
|
|
445
|
+
path: r.path,
|
|
446
|
+
repo: r.repo,
|
|
447
|
+
kind: r.kind,
|
|
448
|
+
lineNo: r.lineNo,
|
|
449
|
+
snippet: r.snippet,
|
|
450
|
+
};
|
|
451
|
+
// Add context for code files if bundleRoot is provided
|
|
452
|
+
if (r.kind === 'code' && bundleRoot) {
|
|
453
|
+
try {
|
|
454
|
+
// r.path is bundleNormRelativePath (e.g., "repos/owner/repo/norm/path/to/file.ts")
|
|
455
|
+
// Construct absolute path to the normalized file
|
|
456
|
+
const filePath = path.join(bundleRoot, r.path);
|
|
457
|
+
// Read file content (use cache to avoid re-reading)
|
|
458
|
+
let fileContent = fileCache.get(filePath);
|
|
459
|
+
if (!fileContent) {
|
|
460
|
+
fileContent = fsSync.readFileSync(filePath, 'utf8');
|
|
461
|
+
fileCache.set(filePath, fileContent);
|
|
462
|
+
}
|
|
463
|
+
// Extract context
|
|
464
|
+
const context = extractContext(fileContent, r.lineNo);
|
|
465
|
+
if (context) {
|
|
466
|
+
hit.context = context;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
// Silently skip context extraction on error (file not found, etc.)
|
|
471
|
+
// Context is optional enhancement, shouldn't break search
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return hit;
|
|
475
|
+
});
|
|
336
476
|
}
|
|
337
477
|
finally {
|
|
338
478
|
db.close();
|
|
@@ -464,9 +604,9 @@ function generateVerificationSummary(claim, supporting, contradicting, related,
|
|
|
464
604
|
* 2. Calculating an overall confidence score
|
|
465
605
|
* 3. Providing a human-readable summary
|
|
466
606
|
*/
|
|
467
|
-
export function verifyClaimInIndex(dbPath, claim, scope, limit) {
|
|
607
|
+
export function verifyClaimInIndex(dbPath, claim, scope, limit, bundleRoot) {
|
|
468
608
|
// Get raw search results
|
|
469
|
-
const rawHits = searchIndex(dbPath, claim, scope, limit);
|
|
609
|
+
const rawHits = searchIndex(dbPath, claim, scope, limit, bundleRoot);
|
|
470
610
|
// Extract tokens from claim for classification
|
|
471
611
|
const claimTokens = tokenizeForSafeQuery(claim);
|
|
472
612
|
// Classify each hit
|