preflight-mcp 0.1.3 → 0.1.4
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 +21 -7
- package/dist/bundle/github.js +34 -3
- package/dist/bundle/githubArchive.js +58 -6
- package/dist/bundle/service.js +224 -5
- package/dist/config.js +1 -0
- package/dist/evidence/dependencyGraph.js +312 -2
- package/dist/jobs/progressTracker.js +191 -0
- package/dist/server.js +261 -36
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -3,7 +3,8 @@ import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mc
|
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import * as z from 'zod';
|
|
5
5
|
import { getConfig } from './config.js';
|
|
6
|
-
import { checkForUpdates, clearBundleMulti, createBundle, findBundleStorageDir, getBundlePathsForId, getEffectiveStorageDir, listBundles, repairBundle, updateBundle, } from './bundle/service.js';
|
|
6
|
+
import { assertBundleComplete, checkForUpdates, checkInProgressLock, clearBundleMulti, computeCreateInputFingerprint, createBundle, findBundleStorageDir, getBundlePathsForId, getEffectiveStorageDir, listBundles, repairBundle, updateBundle, } from './bundle/service.js';
|
|
7
|
+
import { getProgressTracker } from './jobs/progressTracker.js';
|
|
7
8
|
import { readManifest } from './bundle/manifest.js';
|
|
8
9
|
import { safeJoin, toBundleFileUri } from './mcp/uris.js';
|
|
9
10
|
import { wrapPreflightError } from './mcp/errorKinds.js';
|
|
@@ -81,9 +82,12 @@ const RepairBundleInputSchema = {
|
|
|
81
82
|
rebuildGuides: z.boolean().optional().describe('If true, rebuild START_HERE.md and AGENTS.md when missing/empty.'),
|
|
82
83
|
rebuildOverview: z.boolean().optional().describe('If true, rebuild OVERVIEW.md when missing/empty.'),
|
|
83
84
|
};
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
const GetTaskStatusInputSchema = {
|
|
86
|
+
taskId: z.string().optional().describe('Task ID to query (from BUNDLE_IN_PROGRESS error).'),
|
|
87
|
+
fingerprint: z.string().optional().describe('Fingerprint to query (computed from repos/libraries/topics).'),
|
|
88
|
+
repos: z.array(CreateRepoInputSchema).optional().describe('Repos to compute fingerprint from (alternative to fingerprint).'),
|
|
89
|
+
libraries: z.array(z.string()).optional().describe('Libraries for fingerprint computation.'),
|
|
90
|
+
topics: z.array(z.string()).optional().describe('Topics for fingerprint computation.'),
|
|
87
91
|
};
|
|
88
92
|
export async function startServer() {
|
|
89
93
|
const cfg = getConfig();
|
|
@@ -95,7 +99,7 @@ export async function startServer() {
|
|
|
95
99
|
startHttpServer(cfg);
|
|
96
100
|
const server = new McpServer({
|
|
97
101
|
name: 'preflight-mcp',
|
|
98
|
-
version: '0.1.
|
|
102
|
+
version: '0.1.4',
|
|
99
103
|
description: 'Create evidence-based preflight bundles for repositories (docs + code) with SQLite FTS search.',
|
|
100
104
|
}, {
|
|
101
105
|
capabilities: {
|
|
@@ -254,13 +258,20 @@ export async function startServer() {
|
|
|
254
258
|
};
|
|
255
259
|
});
|
|
256
260
|
server.registerTool('preflight_read_file', {
|
|
257
|
-
title: 'Read bundle file',
|
|
258
|
-
description: 'Read
|
|
259
|
-
|
|
261
|
+
title: 'Read bundle file(s)',
|
|
262
|
+
description: 'Read file(s) from bundle. Two modes: ' +
|
|
263
|
+
'(1) Omit "file" param → returns ALL key files (OVERVIEW.md, START_HERE.md, AGENTS.md, manifest.json, repo READMEs) in one call. ' +
|
|
264
|
+
'(2) Provide "file" param → returns that specific file. ' +
|
|
265
|
+
'Use when: "查看bundle", "show bundle", "read overview", "bundle概览", "项目信息".',
|
|
266
|
+
inputSchema: {
|
|
267
|
+
bundleId: z.string().describe('Bundle ID to read.'),
|
|
268
|
+
file: z.string().optional().describe('Specific file to read. If omitted, returns all key files (OVERVIEW.md, START_HERE.md, AGENTS.md, manifest.json, repo READMEs).'),
|
|
269
|
+
},
|
|
260
270
|
outputSchema: {
|
|
261
271
|
bundleId: z.string(),
|
|
262
|
-
file: z.string(),
|
|
263
|
-
content: z.string(),
|
|
272
|
+
file: z.string().optional(),
|
|
273
|
+
content: z.string().optional(),
|
|
274
|
+
files: z.record(z.string(), z.string().nullable()).optional(),
|
|
264
275
|
},
|
|
265
276
|
annotations: {
|
|
266
277
|
readOnlyHint: true,
|
|
@@ -271,16 +282,66 @@ export async function startServer() {
|
|
|
271
282
|
if (!storageDir) {
|
|
272
283
|
throw new Error(`Bundle not found: ${args.bundleId}`);
|
|
273
284
|
}
|
|
274
|
-
const
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
content
|
|
281
|
-
|
|
285
|
+
const paths = getBundlePathsForId(storageDir, args.bundleId);
|
|
286
|
+
const bundleRoot = paths.rootDir;
|
|
287
|
+
// Single file mode
|
|
288
|
+
if (args.file) {
|
|
289
|
+
const absPath = safeJoin(bundleRoot, args.file);
|
|
290
|
+
const content = await fs.readFile(absPath, 'utf8');
|
|
291
|
+
const out = { bundleId: args.bundleId, file: args.file, content };
|
|
292
|
+
return {
|
|
293
|
+
content: [{ type: 'text', text: content }],
|
|
294
|
+
structuredContent: out,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
// Batch mode: read all key files
|
|
298
|
+
const keyFiles = ['OVERVIEW.md', 'START_HERE.md', 'AGENTS.md', 'manifest.json'];
|
|
299
|
+
const files = {};
|
|
300
|
+
for (const file of keyFiles) {
|
|
301
|
+
try {
|
|
302
|
+
const absPath = safeJoin(bundleRoot, file);
|
|
303
|
+
files[file] = await fs.readFile(absPath, 'utf8');
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
files[file] = null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Try to find and read repo README files
|
|
310
|
+
try {
|
|
311
|
+
const manifest = await readManifest(paths.manifestPath);
|
|
312
|
+
for (const repo of manifest.repos ?? []) {
|
|
313
|
+
if (!repo.id)
|
|
314
|
+
continue;
|
|
315
|
+
const [owner, repoName] = repo.id.split('/');
|
|
316
|
+
if (!owner || !repoName)
|
|
317
|
+
continue;
|
|
318
|
+
const readmeNames = ['README.md', 'readme.md', 'Readme.md', 'README.MD'];
|
|
319
|
+
for (const readmeName of readmeNames) {
|
|
320
|
+
const readmePath = `repos/${owner}/${repoName}/norm/${readmeName}`;
|
|
321
|
+
try {
|
|
322
|
+
const absPath = safeJoin(bundleRoot, readmePath);
|
|
323
|
+
files[readmePath] = await fs.readFile(absPath, 'utf8');
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
// Try next
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
// Ignore manifest read errors
|
|
334
|
+
}
|
|
335
|
+
// Build combined text output
|
|
336
|
+
const textParts = [];
|
|
337
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
338
|
+
if (content) {
|
|
339
|
+
textParts.push(`=== ${filePath} ===\n${content}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const out = { bundleId: args.bundleId, files };
|
|
282
343
|
return {
|
|
283
|
-
content: [{ type: 'text', text:
|
|
344
|
+
content: [{ type: 'text', text: textParts.join('\n\n') || '(no files found)' }],
|
|
284
345
|
structuredContent: out,
|
|
285
346
|
};
|
|
286
347
|
}
|
|
@@ -376,6 +437,31 @@ export async function startServer() {
|
|
|
376
437
|
};
|
|
377
438
|
}
|
|
378
439
|
catch (err) {
|
|
440
|
+
// Handle BUNDLE_IN_PROGRESS error specially - provide useful info instead of just error
|
|
441
|
+
if (err?.code === 'BUNDLE_IN_PROGRESS') {
|
|
442
|
+
const elapsedSec = err.startedAt
|
|
443
|
+
? Math.round((Date.now() - new Date(err.startedAt).getTime()) / 1000)
|
|
444
|
+
: 0;
|
|
445
|
+
// Check current progress from tracker
|
|
446
|
+
const tracker = getProgressTracker();
|
|
447
|
+
const task = err.taskId ? tracker.getTask(err.taskId) : undefined;
|
|
448
|
+
const out = {
|
|
449
|
+
status: 'in-progress',
|
|
450
|
+
message: `Bundle creation already in progress. Use preflight_get_task_status to check progress.`,
|
|
451
|
+
taskId: err.taskId,
|
|
452
|
+
fingerprint: err.fingerprint,
|
|
453
|
+
repos: err.repos,
|
|
454
|
+
startedAt: err.startedAt,
|
|
455
|
+
elapsedSeconds: elapsedSec,
|
|
456
|
+
currentPhase: task?.phase,
|
|
457
|
+
currentProgress: task?.progress,
|
|
458
|
+
currentMessage: task?.message,
|
|
459
|
+
};
|
|
460
|
+
return {
|
|
461
|
+
content: [{ type: 'text', text: `⚠️ Bundle creation in progress (${elapsedSec}s elapsed). ${task ? `Current: ${task.phase} (${task.progress}%) - ${task.message}` : 'Use preflight_get_task_status to check progress.'}` }],
|
|
462
|
+
structuredContent: out,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
379
465
|
throw wrapPreflightError(err);
|
|
380
466
|
}
|
|
381
467
|
});
|
|
@@ -486,22 +572,38 @@ export async function startServer() {
|
|
|
486
572
|
structuredContent: out,
|
|
487
573
|
};
|
|
488
574
|
}
|
|
489
|
-
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
575
|
+
// Create task for progress tracking
|
|
576
|
+
const tracker = getProgressTracker();
|
|
577
|
+
const fingerprint = `update-${args.bundleId}`;
|
|
578
|
+
const taskId = tracker.startTask(fingerprint, [args.bundleId]);
|
|
579
|
+
try {
|
|
580
|
+
const { summary, changed } = await updateBundle(cfg, args.bundleId, {
|
|
581
|
+
force: args.force,
|
|
582
|
+
onProgress: (phase, progress, message, total) => {
|
|
583
|
+
tracker.updateProgress(taskId, phase, progress, message, total);
|
|
584
|
+
},
|
|
585
|
+
});
|
|
586
|
+
tracker.completeTask(taskId, args.bundleId);
|
|
587
|
+
const resources = {
|
|
588
|
+
startHere: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'START_HERE.md' }),
|
|
589
|
+
agents: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'AGENTS.md' }),
|
|
590
|
+
overview: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'OVERVIEW.md' }),
|
|
591
|
+
manifest: toBundleFileUri({ bundleId: summary.bundleId, relativePath: 'manifest.json' }),
|
|
592
|
+
};
|
|
593
|
+
const out = {
|
|
594
|
+
changed: args.force ? true : changed,
|
|
595
|
+
...summary,
|
|
596
|
+
resources,
|
|
597
|
+
};
|
|
598
|
+
return {
|
|
599
|
+
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
600
|
+
structuredContent: out,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
catch (updateErr) {
|
|
604
|
+
tracker.failTask(taskId, updateErr instanceof Error ? updateErr.message : String(updateErr));
|
|
605
|
+
throw updateErr;
|
|
606
|
+
}
|
|
505
607
|
}
|
|
506
608
|
catch (err) {
|
|
507
609
|
throw wrapPreflightError(err);
|
|
@@ -607,6 +709,8 @@ export async function startServer() {
|
|
|
607
709
|
},
|
|
608
710
|
}, async (args) => {
|
|
609
711
|
try {
|
|
712
|
+
// Check bundle completeness before any operation
|
|
713
|
+
await assertBundleComplete(cfg, args.bundleId);
|
|
610
714
|
// Resolve bundle location across storageDirs (more robust than a single effectiveDir).
|
|
611
715
|
const storageDir = await findBundleStorageDir(cfg.storageDirs, args.bundleId);
|
|
612
716
|
if (!storageDir) {
|
|
@@ -643,7 +747,11 @@ export async function startServer() {
|
|
|
643
747
|
});
|
|
644
748
|
server.registerTool('preflight_evidence_dependency_graph', {
|
|
645
749
|
title: 'Evidence: dependency graph (callers + imports)',
|
|
646
|
-
description: 'Generate an evidence-based dependency graph
|
|
750
|
+
description: 'Generate an evidence-based dependency graph. Two modes: ' +
|
|
751
|
+
'(1) TARGET MODE: provide target.file to analyze a specific file\'s imports and callers. ' +
|
|
752
|
+
'(2) GLOBAL MODE: omit target to generate a project-wide import graph of all code files. ' +
|
|
753
|
+
'For target mode, file path must be bundle-relative: repos/{owner}/{repo}/norm/{path}. ' +
|
|
754
|
+
'Use preflight_search_bundle to find file paths, or check OVERVIEW.md.',
|
|
647
755
|
inputSchema: DependencyGraphInputSchema,
|
|
648
756
|
outputSchema: {
|
|
649
757
|
meta: z.any(),
|
|
@@ -655,6 +763,8 @@ export async function startServer() {
|
|
|
655
763
|
},
|
|
656
764
|
}, async (args) => {
|
|
657
765
|
try {
|
|
766
|
+
// Check bundle completeness before generating dependency graph
|
|
767
|
+
await assertBundleComplete(cfg, args.bundleId);
|
|
658
768
|
const out = await generateDependencyGraph(cfg, args);
|
|
659
769
|
return {
|
|
660
770
|
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
@@ -679,6 +789,8 @@ export async function startServer() {
|
|
|
679
789
|
},
|
|
680
790
|
}, async (args) => {
|
|
681
791
|
try {
|
|
792
|
+
// Check bundle completeness before trace upsert
|
|
793
|
+
await assertBundleComplete(cfg, args.bundleId);
|
|
682
794
|
const out = await traceUpsert(cfg, args);
|
|
683
795
|
return {
|
|
684
796
|
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
@@ -715,6 +827,10 @@ export async function startServer() {
|
|
|
715
827
|
},
|
|
716
828
|
}, async (args) => {
|
|
717
829
|
try {
|
|
830
|
+
// Check bundle completeness if bundleId is provided
|
|
831
|
+
if (args.bundleId) {
|
|
832
|
+
await assertBundleComplete(cfg, args.bundleId);
|
|
833
|
+
}
|
|
718
834
|
const out = await traceQuery(cfg, args);
|
|
719
835
|
return {
|
|
720
836
|
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
@@ -763,6 +879,115 @@ export async function startServer() {
|
|
|
763
879
|
throw wrapPreflightError(err);
|
|
764
880
|
}
|
|
765
881
|
});
|
|
882
|
+
// Get task status - for checking progress of in-progress bundle creations
|
|
883
|
+
server.registerTool('preflight_get_task_status', {
|
|
884
|
+
title: 'Get task status',
|
|
885
|
+
description: 'Check status of bundle creation tasks (especially in-progress ones). Use when: "check bundle creation progress", "what is the status", "查看任务状态", "下载进度". Can query by taskId (from error), fingerprint, or repos.',
|
|
886
|
+
inputSchema: GetTaskStatusInputSchema,
|
|
887
|
+
outputSchema: {
|
|
888
|
+
found: z.boolean(),
|
|
889
|
+
task: z.object({
|
|
890
|
+
taskId: z.string(),
|
|
891
|
+
fingerprint: z.string(),
|
|
892
|
+
phase: z.string(),
|
|
893
|
+
progress: z.number(),
|
|
894
|
+
total: z.number().optional(),
|
|
895
|
+
message: z.string(),
|
|
896
|
+
startedAt: z.string(),
|
|
897
|
+
updatedAt: z.string(),
|
|
898
|
+
repos: z.array(z.string()),
|
|
899
|
+
bundleId: z.string().optional(),
|
|
900
|
+
error: z.string().optional(),
|
|
901
|
+
}).optional(),
|
|
902
|
+
inProgressLock: z.object({
|
|
903
|
+
bundleId: z.string(),
|
|
904
|
+
status: z.string(),
|
|
905
|
+
startedAt: z.string().optional(),
|
|
906
|
+
taskId: z.string().optional(),
|
|
907
|
+
repos: z.array(z.string()).optional(),
|
|
908
|
+
elapsedSeconds: z.number().optional(),
|
|
909
|
+
}).optional(),
|
|
910
|
+
activeTasks: z.array(z.object({
|
|
911
|
+
taskId: z.string(),
|
|
912
|
+
fingerprint: z.string(),
|
|
913
|
+
phase: z.string(),
|
|
914
|
+
progress: z.number(),
|
|
915
|
+
message: z.string(),
|
|
916
|
+
repos: z.array(z.string()),
|
|
917
|
+
startedAt: z.string(),
|
|
918
|
+
})).optional(),
|
|
919
|
+
},
|
|
920
|
+
annotations: {
|
|
921
|
+
readOnlyHint: true,
|
|
922
|
+
},
|
|
923
|
+
}, async (args) => {
|
|
924
|
+
try {
|
|
925
|
+
const tracker = getProgressTracker();
|
|
926
|
+
let result = { found: false };
|
|
927
|
+
// Compute fingerprint if repos provided
|
|
928
|
+
let fingerprint = args.fingerprint;
|
|
929
|
+
if (!fingerprint && args.repos?.length) {
|
|
930
|
+
fingerprint = computeCreateInputFingerprint({
|
|
931
|
+
repos: args.repos,
|
|
932
|
+
libraries: args.libraries,
|
|
933
|
+
topics: args.topics,
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
// Query by taskId
|
|
937
|
+
if (args.taskId) {
|
|
938
|
+
const task = tracker.getTask(args.taskId);
|
|
939
|
+
if (task) {
|
|
940
|
+
result = { found: true, task };
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
// Query by fingerprint
|
|
944
|
+
else if (fingerprint) {
|
|
945
|
+
const task = tracker.getTaskByFingerprint(fingerprint);
|
|
946
|
+
if (task) {
|
|
947
|
+
result = { found: true, task };
|
|
948
|
+
}
|
|
949
|
+
// Also check persistent in-progress lock
|
|
950
|
+
const lock = await checkInProgressLock(cfg, fingerprint);
|
|
951
|
+
if (lock) {
|
|
952
|
+
const elapsedSeconds = lock.startedAt
|
|
953
|
+
? Math.round((Date.now() - new Date(lock.startedAt).getTime()) / 1000)
|
|
954
|
+
: undefined;
|
|
955
|
+
result.inProgressLock = {
|
|
956
|
+
bundleId: lock.bundleId,
|
|
957
|
+
status: lock.status ?? 'unknown',
|
|
958
|
+
startedAt: lock.startedAt,
|
|
959
|
+
taskId: lock.taskId,
|
|
960
|
+
repos: lock.repos,
|
|
961
|
+
elapsedSeconds,
|
|
962
|
+
};
|
|
963
|
+
result.found = true;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
// If no specific query, return all active tasks
|
|
967
|
+
else {
|
|
968
|
+
const activeTasks = tracker.listActiveTasks();
|
|
969
|
+
if (activeTasks.length > 0) {
|
|
970
|
+
result = { found: true, activeTasks };
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
const summary = result.found
|
|
974
|
+
? result.task
|
|
975
|
+
? `Task ${result.task.taskId}: ${result.task.phase} (${result.task.progress}%) - ${result.task.message}`
|
|
976
|
+
: result.activeTasks
|
|
977
|
+
? `${result.activeTasks.length} active task(s)`
|
|
978
|
+
: result.inProgressLock
|
|
979
|
+
? `In-progress lock found (started ${result.inProgressLock.elapsedSeconds}s ago)`
|
|
980
|
+
: 'Status found'
|
|
981
|
+
: 'No matching task found';
|
|
982
|
+
return {
|
|
983
|
+
content: [{ type: 'text', text: summary }],
|
|
984
|
+
structuredContent: result,
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
catch (err) {
|
|
988
|
+
throw wrapPreflightError(err);
|
|
989
|
+
}
|
|
990
|
+
});
|
|
766
991
|
// Provide backward-compatible parsing of the same URI via resources/read for clients that bypass templates.
|
|
767
992
|
// This is a safety net: if a client gives us a fully-specified URI, we can still serve it.
|
|
768
993
|
server.registerResource('bundle-file-compat', 'preflight://bundle-file', {
|