preflight-mcp 0.4.0 → 0.4.2

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.
@@ -20,6 +20,50 @@ const DEDUP_INDEX_FILE = '.preflight-dedup-index.json';
20
20
  function sha256Hex(text) {
21
21
  return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
22
22
  }
23
+ /**
24
+ * Parse a skipped file string (from ingest) into structured SkippedFileEntry.
25
+ * Formats:
26
+ * - "path/to/file (too large: 12345 bytes)"
27
+ * - "path/to/file (binary)"
28
+ * - "path/to/file (non-utf8)"
29
+ * - "(bundle maxTotalBytes reached) stopped before: path/to/file"
30
+ */
31
+ function parseSkippedString(s, repoId) {
32
+ // Pattern: "path (too large: 12345 bytes)"
33
+ const tooLargeMatch = s.match(/^(.+?) \(too large: (\d+) bytes\)$/);
34
+ if (tooLargeMatch) {
35
+ return {
36
+ path: `${repoId}/${tooLargeMatch[1]}`,
37
+ reason: 'too_large',
38
+ size: parseInt(tooLargeMatch[2], 10),
39
+ };
40
+ }
41
+ // Pattern: "path (binary)"
42
+ const binaryMatch = s.match(/^(.+?) \(binary\)$/);
43
+ if (binaryMatch) {
44
+ return {
45
+ path: `${repoId}/${binaryMatch[1]}`,
46
+ reason: 'binary',
47
+ };
48
+ }
49
+ // Pattern: "path (non-utf8)"
50
+ const nonUtf8Match = s.match(/^(.+?) \(non-utf8\)$/);
51
+ if (nonUtf8Match) {
52
+ return {
53
+ path: `${repoId}/${nonUtf8Match[1]}`,
54
+ reason: 'non_utf8',
55
+ };
56
+ }
57
+ // Pattern: "(bundle maxTotalBytes reached) stopped before: path"
58
+ const maxTotalMatch = s.match(/^\(bundle maxTotalBytes reached\) stopped before: (.+)$/);
59
+ if (maxTotalMatch) {
60
+ return {
61
+ path: `${repoId}/${maxTotalMatch[1]}`,
62
+ reason: 'max_total_reached',
63
+ };
64
+ }
65
+ return null;
66
+ }
23
67
  function normalizeList(values) {
24
68
  return (values ?? [])
25
69
  .map((s) => s.trim())
@@ -939,6 +983,7 @@ async function createBundleInternal(cfg, input, options) {
939
983
  await ensureDir(tmpPaths.rootDir);
940
984
  const finalPaths = getBundlePaths(effectiveStorageDir, bundleId);
941
985
  const allIngestedFiles = [];
986
+ const allSkippedFiles = [];
942
987
  const reposSummary = [];
943
988
  const allWarnings = [];
944
989
  // Track temp checkout directory for cleanup
@@ -970,9 +1015,16 @@ async function createBundleInternal(cfg, input, options) {
970
1015
  });
971
1016
  allIngestedFiles.push(...files);
972
1017
  allWarnings.push(...warnings);
1018
+ // Parse and collect skipped files
1019
+ const repoId = `${owner}/${repo}`;
1020
+ for (const s of skipped) {
1021
+ const entry = parseSkippedString(s, repoId);
1022
+ if (entry)
1023
+ allSkippedFiles.push(entry);
1024
+ }
973
1025
  reposSummary.push({
974
1026
  kind: 'github',
975
- id: `${owner}/${repo}`,
1027
+ id: repoId,
976
1028
  source,
977
1029
  headSha,
978
1030
  notes: [...notes, ...skipped].slice(0, 50),
@@ -993,7 +1045,14 @@ async function createBundleInternal(cfg, input, options) {
993
1045
  ref: repoInput.ref,
994
1046
  });
995
1047
  allIngestedFiles.push(...files);
996
- reposSummary.push({ kind: 'local', id: `${owner}/${repo}`, source: 'local', notes: skipped.slice(0, 50) });
1048
+ // Parse and collect skipped files
1049
+ const repoId = `${owner}/${repo}`;
1050
+ for (const s of skipped) {
1051
+ const entry = parseSkippedString(s, repoId);
1052
+ if (entry)
1053
+ allSkippedFiles.push(entry);
1054
+ }
1055
+ reposSummary.push({ kind: 'local', id: repoId, source: 'local', notes: skipped.slice(0, 50) });
997
1056
  }
998
1057
  }
999
1058
  // Context7 libraries (best-effort).
@@ -1061,6 +1120,8 @@ async function createBundleInternal(cfg, input, options) {
1061
1120
  includeDocs: true,
1062
1121
  includeCode: true,
1063
1122
  },
1123
+ // Store skipped files for transparency (limit to 200 entries to avoid bloat)
1124
+ skippedFiles: allSkippedFiles.length > 0 ? allSkippedFiles.slice(0, 200) : undefined,
1064
1125
  };
1065
1126
  await writeManifest(tmpPaths.manifestPath, manifest);
1066
1127
  // Guides.
package/dist/server.js CHANGED
@@ -134,7 +134,7 @@ export async function startServer() {
134
134
  startHttpServer(cfg);
135
135
  const server = new McpServer({
136
136
  name: 'preflight-mcp',
137
- version: '0.4.0',
137
+ version: '0.4.2',
138
138
  description: 'Create evidence-based preflight bundles for repositories (docs + code) with SQLite FTS search.',
139
139
  }, {
140
140
  capabilities: {
@@ -547,6 +547,7 @@ export async function startServer() {
547
547
  focusDir: z.string().optional().describe('Focus directory path - expand deeper within this path (e.g., "owner/repo/norm/src"). Gets +3 extra depth levels.'),
548
548
  focusDepthBonus: z.number().int().min(1).max(6).optional().describe('Extra depth levels for focusDir. Default 3.'),
549
549
  showFileCountPerDir: z.boolean().optional().describe('If true, include file count per directory in stats.byDir.'),
550
+ showSkippedFiles: z.boolean().optional().describe('If true, include list of files that were skipped during indexing (too large, binary, etc.). Helps understand what content is NOT searchable.'),
550
551
  },
551
552
  outputSchema: {
552
553
  bundleId: z.string(),
@@ -563,6 +564,11 @@ export async function startServer() {
563
564
  type: z.enum(['readme', 'main', 'index', 'cli', 'server', 'app', 'test', 'config']),
564
565
  priority: z.number(),
565
566
  })),
567
+ skippedFiles: z.array(z.object({
568
+ path: z.string(),
569
+ reason: z.string(),
570
+ size: z.number().optional(),
571
+ })).optional().describe('Files skipped during indexing (only when showSkippedFiles=true). These files are NOT searchable.'),
566
572
  },
567
573
  annotations: {
568
574
  readOnlyHint: true,
@@ -582,10 +588,45 @@ export async function startServer() {
582
588
  focusDepthBonus: args.focusDepthBonus,
583
589
  showFileCountPerDir: args.showFileCountPerDir,
584
590
  });
591
+ // Add skipped files if requested
592
+ let skippedFiles;
593
+ if (args.showSkippedFiles) {
594
+ try {
595
+ const manifest = await readManifest(paths.manifestPath);
596
+ if (manifest.skippedFiles && manifest.skippedFiles.length > 0) {
597
+ const reasonLabels = {
598
+ too_large: 'too large',
599
+ binary: 'binary file',
600
+ non_utf8: 'non-UTF8 encoding',
601
+ max_total_reached: 'bundle size limit reached',
602
+ };
603
+ skippedFiles = manifest.skippedFiles.map(s => ({
604
+ path: s.path,
605
+ reason: reasonLabels[s.reason] ?? s.reason,
606
+ size: s.size,
607
+ }));
608
+ }
609
+ }
610
+ catch {
611
+ // Ignore manifest read errors
612
+ }
613
+ }
585
614
  const textOutput = formatTreeResult(result);
615
+ let fullTextOutput = textOutput;
616
+ if (skippedFiles && skippedFiles.length > 0) {
617
+ fullTextOutput += `\n\n## Skipped Files (${skippedFiles.length} files not searchable)\n`;
618
+ for (const sf of skippedFiles.slice(0, 20)) {
619
+ const sizeStr = sf.size ? ` (${(sf.size / 1024).toFixed(0)}KB)` : '';
620
+ fullTextOutput += `- ${sf.path}: ${sf.reason}${sizeStr}\n`;
621
+ }
622
+ if (skippedFiles.length > 20) {
623
+ fullTextOutput += `... and ${skippedFiles.length - 20} more\n`;
624
+ }
625
+ }
626
+ const structuredResult = { ...result, skippedFiles };
586
627
  return {
587
- content: [{ type: 'text', text: textOutput }],
588
- structuredContent: result,
628
+ content: [{ type: 'text', text: fullTextOutput }],
629
+ structuredContent: structuredResult,
589
630
  };
590
631
  }
591
632
  catch (err) {
@@ -708,7 +749,11 @@ export async function startServer() {
708
749
  });
709
750
  server.registerTool('preflight_create_bundle', {
710
751
  title: 'Create bundle',
711
- description: 'Create a new bundle from GitHub repos or local directories. IMPORTANT: Only call this tool when the user EXPLICITLY asks to create/index a repo. Do NOT automatically create bundles when search fails or bundle is not found - ASK the user first! Use when user says: "index this repo", "create bundle for", "创建bundle", "添加GitHub项目". If ifExists=updateExisting, updates an existing bundle.',
752
+ description: 'Create a new bundle from GitHub repos or local directories. ' +
753
+ '**Safe to call proactively** - use `ifExists: "returnExisting"` to avoid duplicates. ' +
754
+ 'Bundle creation is a **read-only collection** operation (clones repo, builds index, generates guides). ' +
755
+ 'When user asks to analyze/understand a project, create the bundle first if it does not exist. ' +
756
+ 'Use when: "analyze this repo", "understand this codebase", "index project", "分析项目", "理解代码".',
712
757
  inputSchema: CreateBundleInputSchema,
713
758
  outputSchema: {
714
759
  // Normal completion fields
@@ -1084,6 +1129,16 @@ export async function startServer() {
1084
1129
  meta: z.object({
1085
1130
  tokenBudgetHint: z.string().optional().describe('Hint about token savings from groupByFile.'),
1086
1131
  }).optional().describe('Search metadata for EDDA optimization.'),
1132
+ // Skipped files hint for transparency
1133
+ skippedFilesHint: z.object({
1134
+ message: z.string().describe('Human-readable hint explaining why some content might be missing.'),
1135
+ skippedCount: z.number().describe('Total number of files skipped during indexing.'),
1136
+ examples: z.array(z.object({
1137
+ path: z.string(),
1138
+ reason: z.string(),
1139
+ size: z.number().optional(),
1140
+ })).describe('Example skipped files (limited to 5).'),
1141
+ }).optional().describe('Present when search returns 0 results and files were skipped during indexing. Helps explain why expected content might be missing.'),
1087
1142
  autoUpdated: z
1088
1143
  .boolean()
1089
1144
  .optional()
@@ -1130,6 +1185,34 @@ export async function startServer() {
1130
1185
  });
1131
1186
  }
1132
1187
  const paths = getBundlePathsForId(storageDir, args.bundleId);
1188
+ // Helper to build skippedFilesHint when search returns 0 results
1189
+ const buildSkippedFilesHint = async () => {
1190
+ try {
1191
+ const manifest = await readManifest(paths.manifestPath);
1192
+ if (!manifest.skippedFiles || manifest.skippedFiles.length === 0) {
1193
+ return undefined;
1194
+ }
1195
+ const skipped = manifest.skippedFiles;
1196
+ const reasonLabels = {
1197
+ too_large: 'too large',
1198
+ binary: 'binary file',
1199
+ non_utf8: 'non-UTF8 encoding',
1200
+ max_total_reached: 'bundle size limit reached',
1201
+ };
1202
+ return {
1203
+ message: `Search returned 0 results. Note: ${skipped.length} file(s) were skipped during indexing and are not searchable. Check if your target content might be in a skipped file.`,
1204
+ skippedCount: skipped.length,
1205
+ examples: skipped.slice(0, 5).map(s => ({
1206
+ path: s.path,
1207
+ reason: reasonLabels[s.reason] ?? s.reason,
1208
+ size: s.size,
1209
+ })),
1210
+ };
1211
+ }
1212
+ catch {
1213
+ return undefined;
1214
+ }
1215
+ };
1133
1216
  // Use advanced search if EDDA features are requested
1134
1217
  const useAdvanced = args.groupByFile || args.fileTypeFilters?.length || args.includeScore;
1135
1218
  if (useAdvanced) {
@@ -1178,6 +1261,14 @@ export async function startServer() {
1178
1261
  if (warnings.length > 0) {
1179
1262
  out.warnings = warnings;
1180
1263
  }
1264
+ // Add skippedFilesHint when 0 results
1265
+ const hasNoResults = result.hits.length === 0 && (!grouped || grouped.length === 0);
1266
+ if (hasNoResults) {
1267
+ const hint = await buildSkippedFilesHint();
1268
+ if (hint) {
1269
+ out.skippedFilesHint = hint;
1270
+ }
1271
+ }
1181
1272
  return {
1182
1273
  content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
1183
1274
  structuredContent: out,
@@ -1228,6 +1319,13 @@ export async function startServer() {
1228
1319
  if (warnings.length > 0) {
1229
1320
  out.warnings = warnings;
1230
1321
  }
1322
+ // Add skippedFilesHint when 0 results
1323
+ if (hits.length === 0) {
1324
+ const hint = await buildSkippedFilesHint();
1325
+ if (hint) {
1326
+ out.skippedFilesHint = hint;
1327
+ }
1328
+ }
1231
1329
  return {
1232
1330
  content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
1233
1331
  structuredContent: out,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "preflight-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "MCP server that creates evidence-based preflight bundles for GitHub repositories and library docs.",
5
5
  "type": "module",
6
6
  "license": "MIT",