preflight-mcp 0.5.2 → 0.5.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 +23 -4
- package/README.zh-CN.md +21 -2
- package/dist/ast/treeSitter.js +610 -0
- package/dist/server.js +131 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ Preflight: 🔗 Trace links:
|
|
|
51
51
|
- 📖 **Auto-generated guides** — `START_HERE.md`, `AGENTS.md`, `OVERVIEW.md`
|
|
52
52
|
- ☁️ **Cloud sync** — Multi-path mirror backup for redundancy
|
|
53
53
|
- 🧠 **EDDA (Evidence-Driven Deep Analysis)** — Auto-generate auditable claims with evidence
|
|
54
|
-
- ⚡ **
|
|
54
|
+
- ⚡ **21 MCP tools + 5 prompts** — Complete toolkit for code exploration
|
|
55
55
|
- 📄 **Cursor pagination** — Handle large result sets efficiently (RFC v2)
|
|
56
56
|
|
|
57
57
|
<details>
|
|
@@ -76,7 +76,7 @@ Preflight: 🔗 Trace links:
|
|
|
76
76
|
- [Demo](#demo)
|
|
77
77
|
- [Core Features](#core-features)
|
|
78
78
|
- [Quick Start](#quick-start)
|
|
79
|
-
- [Tools](#tools-
|
|
79
|
+
- [Tools](#tools-21-total)
|
|
80
80
|
- [Prompts](#prompts-5-total)
|
|
81
81
|
- [Environment Variables](#environment-variables)
|
|
82
82
|
- [Contributing](#contributing)
|
|
@@ -161,7 +161,7 @@ Run end-to-end smoke test:
|
|
|
161
161
|
npm run smoke
|
|
162
162
|
```
|
|
163
163
|
|
|
164
|
-
## Tools (
|
|
164
|
+
## Tools (21 total)
|
|
165
165
|
|
|
166
166
|
### `preflight_list_bundles`
|
|
167
167
|
List bundle IDs in storage.
|
|
@@ -191,12 +191,31 @@ Input (example):
|
|
|
191
191
|
**Note**: If the bundle contains code files, consider using `preflight_evidence_dependency_graph` for dependency analysis or `preflight_trace_upsert` for trace links.
|
|
192
192
|
|
|
193
193
|
### `preflight_read_file`
|
|
194
|
-
Read file(s) from bundle.
|
|
194
|
+
Read file(s) from bundle. Multiple modes:
|
|
195
195
|
- **Batch mode** (omit `file`): Returns ALL key files (OVERVIEW.md, START_HERE.md, AGENTS.md, manifest.json, deps/dependency-graph.json, repo READMEs) in one call
|
|
196
196
|
- **Single file mode** (provide `file`): Returns that specific file
|
|
197
197
|
- **Evidence citation**: Use `withLineNumbers: true` to get `N|line` format; use `ranges: ["20-80"]` to read specific lines
|
|
198
198
|
- Triggers: "查看bundle", "bundle概览", "项目信息", "show bundle", "读取依赖图"
|
|
199
199
|
|
|
200
|
+
**NEW in v0.5.3 - Symbol outline & reading:**
|
|
201
|
+
- `outline: true`: Returns symbol structure (function/class/method/interface/type/enum) with line ranges
|
|
202
|
+
- Supports: `.ts`, `.tsx`, `.js`, `.jsx`, `.py`, `.go`, `.rs`
|
|
203
|
+
- 90%+ token savings for understanding file structure
|
|
204
|
+
- `symbol: "name"`: Read a specific symbol by name
|
|
205
|
+
- Format: `"functionName"` or `"ClassName.methodName"`
|
|
206
|
+
- Auto-includes context lines and returns with line numbers
|
|
207
|
+
|
|
208
|
+
**Example - Outline mode:**
|
|
209
|
+
```
|
|
210
|
+
[src/server.ts] Outline (15 top-level symbols, typescript):
|
|
211
|
+
├── ⚡function startServer(): Promise<void> :174-200
|
|
212
|
+
├── ⚡class McpServer :205-400
|
|
213
|
+
│ ├── method registerTool :210-250
|
|
214
|
+
│ └── method start :380-400
|
|
215
|
+
└── ⚡interface Config :45-71
|
|
216
|
+
```
|
|
217
|
+
(`⚡` = exported)
|
|
218
|
+
|
|
200
219
|
### `preflight_repo_tree`
|
|
201
220
|
Get repository structure overview without wasting tokens on search.
|
|
202
221
|
- Returns: ASCII directory tree, file count by extension/directory, entry point candidates
|
package/README.zh-CN.md
CHANGED
|
@@ -209,12 +209,31 @@ npm run smoke
|
|
|
209
209
|
**💡 提示**:对于代码仓库,创建 bundle 后可进一步使用 `preflight_evidence_dependency_graph` 获取依赖图,或使用 `preflight_trace_upsert` 记录代码←→需求/测试的追溯链接。
|
|
210
210
|
|
|
211
211
|
### `preflight_read_file`
|
|
212
|
-
从 bundle
|
|
213
|
-
- **批量模式**(省略 `file
|
|
212
|
+
从 bundle 读取文件。多种模式:
|
|
213
|
+
- **批量模式**(省略 `file`):返回所有关键文件(OVERVIEW.md、START_HERE.md、AGENTS.md、manifest.json 等)
|
|
214
214
|
- **单文件模式**(提供 `file`):返回指定文件
|
|
215
215
|
- **证据引用**:使用 `withLineNumbers: true` 获取 `N|行` 格式;使用 `ranges: ["20-80"]` 读取指定行
|
|
216
216
|
- 触发词:「查看概览」「项目概览」「bundle详情」「读取依赖图」
|
|
217
217
|
|
|
218
|
+
**v0.5.3 新增 - 符号大纲与按符号读取:**
|
|
219
|
+
- `outline: true`:返回文件的符号结构(function/class/method/interface/type/enum),附带行号范围
|
|
220
|
+
- 支持:`.ts`、`.tsx`、`.js`、`.jsx`、`.py`、`.go`、`.rs`
|
|
221
|
+
- 理解文件结构可节省 90%+ token
|
|
222
|
+
- `symbol: "name"`:按名称读取特定符号
|
|
223
|
+
- 格式:`"functionName"` 或 `"ClassName.methodName"`
|
|
224
|
+
- 自动包含上下文行并返回带行号的内容
|
|
225
|
+
|
|
226
|
+
**示例 - 大纲模式:**
|
|
227
|
+
```
|
|
228
|
+
[src/server.ts] Outline (15 top-level symbols, typescript):
|
|
229
|
+
├── ⚡function startServer(): Promise<void> :174-200
|
|
230
|
+
├── ⚡class McpServer :205-400
|
|
231
|
+
│ ├── method registerTool :210-250
|
|
232
|
+
│ └── method start :380-400
|
|
233
|
+
└── ⚡interface Config :45-71
|
|
234
|
+
```
|
|
235
|
+
(`⚡` = 已导出)
|
|
236
|
+
|
|
218
237
|
### `preflight_repo_tree`
|
|
219
238
|
获取仓库结构概览,避免浪费 token 搜索。
|
|
220
239
|
- 返回:ASCII 目录树、按扩展名/目录统计文件数、入口点候选
|
package/dist/ast/treeSitter.js
CHANGED
|
@@ -586,3 +586,613 @@ export async function extractExportedSymbolsWasm(filePath, normalizedContent) {
|
|
|
586
586
|
return null;
|
|
587
587
|
return { language: res.language, exports: res.exports };
|
|
588
588
|
}
|
|
589
|
+
// ============================================================================
|
|
590
|
+
// Outline extraction (symbol structure for code navigation)
|
|
591
|
+
// ============================================================================
|
|
592
|
+
/**
|
|
593
|
+
* Check if a node is preceded by an export keyword (for JS/TS).
|
|
594
|
+
*/
|
|
595
|
+
function isExportedJsTs(node) {
|
|
596
|
+
const parent = node.parent;
|
|
597
|
+
if (!parent)
|
|
598
|
+
return false;
|
|
599
|
+
// Direct export: export function foo() {}
|
|
600
|
+
if (parent.type === 'export_statement')
|
|
601
|
+
return true;
|
|
602
|
+
// export default function foo() {}
|
|
603
|
+
if (parent.type === 'export_statement' && parent.text.includes('default'))
|
|
604
|
+
return true;
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Extract function signature from a function declaration node.
|
|
609
|
+
*/
|
|
610
|
+
function extractFunctionSignatureJsTs(node) {
|
|
611
|
+
const params = node.childForFieldName('parameters');
|
|
612
|
+
const returnType = node.childForFieldName('return_type');
|
|
613
|
+
if (!params)
|
|
614
|
+
return undefined;
|
|
615
|
+
let sig = params.text;
|
|
616
|
+
if (returnType) {
|
|
617
|
+
sig += `: ${returnType.text}`;
|
|
618
|
+
}
|
|
619
|
+
return sig;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Extract method signature from a method definition node.
|
|
623
|
+
*/
|
|
624
|
+
function extractMethodSignatureJsTs(node) {
|
|
625
|
+
const params = node.childForFieldName('parameters');
|
|
626
|
+
const returnType = node.childForFieldName('return_type');
|
|
627
|
+
if (!params)
|
|
628
|
+
return undefined;
|
|
629
|
+
let sig = params.text;
|
|
630
|
+
if (returnType) {
|
|
631
|
+
sig += `: ${returnType.text}`;
|
|
632
|
+
}
|
|
633
|
+
return sig;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Extract class methods as children.
|
|
637
|
+
*/
|
|
638
|
+
function extractClassMethodsJsTs(classNode) {
|
|
639
|
+
const methods = [];
|
|
640
|
+
const body = classNode.childForFieldName('body');
|
|
641
|
+
if (!body)
|
|
642
|
+
return methods;
|
|
643
|
+
for (const child of body.namedChildren) {
|
|
644
|
+
if (child.type === 'method_definition' || child.type === 'public_field_definition') {
|
|
645
|
+
const name = child.childForFieldName('name');
|
|
646
|
+
if (!name)
|
|
647
|
+
continue;
|
|
648
|
+
const isMethod = child.type === 'method_definition';
|
|
649
|
+
methods.push({
|
|
650
|
+
kind: isMethod ? 'method' : 'variable',
|
|
651
|
+
name: name.text,
|
|
652
|
+
signature: isMethod ? extractMethodSignatureJsTs(child) : undefined,
|
|
653
|
+
range: {
|
|
654
|
+
startLine: child.startPosition.row + 1,
|
|
655
|
+
endLine: child.endPosition.row + 1,
|
|
656
|
+
},
|
|
657
|
+
exported: true, // Class members are implicitly accessible
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return methods;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Extract outline from JavaScript/TypeScript/TSX files.
|
|
665
|
+
*/
|
|
666
|
+
function extractOutlineJsTs(root, lang) {
|
|
667
|
+
const outline = [];
|
|
668
|
+
// Collect all exported names for checking
|
|
669
|
+
const exportedNames = new Set();
|
|
670
|
+
for (const st of root.descendantsOfType('export_statement')) {
|
|
671
|
+
// export { name1, name2 }
|
|
672
|
+
const clause = st.descendantsOfType('export_clause')[0];
|
|
673
|
+
if (clause) {
|
|
674
|
+
for (const spec of clause.descendantsOfType('export_specifier')) {
|
|
675
|
+
const name = spec.childForFieldName('name');
|
|
676
|
+
if (name)
|
|
677
|
+
exportedNames.add(name.text);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
for (const child of root.namedChildren) {
|
|
682
|
+
// Handle export statements
|
|
683
|
+
let actualNode = child;
|
|
684
|
+
let isExported = false;
|
|
685
|
+
if (child.type === 'export_statement') {
|
|
686
|
+
isExported = true;
|
|
687
|
+
// Get the actual declaration inside
|
|
688
|
+
const decl = child.namedChildren.find(n => n.type !== 'export_clause' &&
|
|
689
|
+
n.type !== 'string' &&
|
|
690
|
+
n.type !== 'comment');
|
|
691
|
+
if (decl) {
|
|
692
|
+
actualNode = decl;
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
continue; // export { ... } without declaration
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
const name = actualNode.childForFieldName('name');
|
|
699
|
+
switch (actualNode.type) {
|
|
700
|
+
case 'function_declaration': {
|
|
701
|
+
if (!name)
|
|
702
|
+
continue;
|
|
703
|
+
outline.push({
|
|
704
|
+
kind: 'function',
|
|
705
|
+
name: name.text,
|
|
706
|
+
signature: extractFunctionSignatureJsTs(actualNode),
|
|
707
|
+
range: {
|
|
708
|
+
startLine: actualNode.startPosition.row + 1,
|
|
709
|
+
endLine: actualNode.endPosition.row + 1,
|
|
710
|
+
},
|
|
711
|
+
exported: isExported || exportedNames.has(name.text),
|
|
712
|
+
});
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
case 'class_declaration': {
|
|
716
|
+
if (!name)
|
|
717
|
+
continue;
|
|
718
|
+
const children = extractClassMethodsJsTs(actualNode);
|
|
719
|
+
outline.push({
|
|
720
|
+
kind: 'class',
|
|
721
|
+
name: name.text,
|
|
722
|
+
range: {
|
|
723
|
+
startLine: actualNode.startPosition.row + 1,
|
|
724
|
+
endLine: actualNode.endPosition.row + 1,
|
|
725
|
+
},
|
|
726
|
+
exported: isExported || exportedNames.has(name.text),
|
|
727
|
+
children: children.length > 0 ? children : undefined,
|
|
728
|
+
});
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
case 'interface_declaration': {
|
|
732
|
+
if (!name)
|
|
733
|
+
continue;
|
|
734
|
+
outline.push({
|
|
735
|
+
kind: 'interface',
|
|
736
|
+
name: name.text,
|
|
737
|
+
range: {
|
|
738
|
+
startLine: actualNode.startPosition.row + 1,
|
|
739
|
+
endLine: actualNode.endPosition.row + 1,
|
|
740
|
+
},
|
|
741
|
+
exported: isExported || exportedNames.has(name.text),
|
|
742
|
+
});
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
case 'type_alias_declaration': {
|
|
746
|
+
if (!name)
|
|
747
|
+
continue;
|
|
748
|
+
outline.push({
|
|
749
|
+
kind: 'type',
|
|
750
|
+
name: name.text,
|
|
751
|
+
range: {
|
|
752
|
+
startLine: actualNode.startPosition.row + 1,
|
|
753
|
+
endLine: actualNode.endPosition.row + 1,
|
|
754
|
+
},
|
|
755
|
+
exported: isExported || exportedNames.has(name.text),
|
|
756
|
+
});
|
|
757
|
+
break;
|
|
758
|
+
}
|
|
759
|
+
case 'enum_declaration': {
|
|
760
|
+
if (!name)
|
|
761
|
+
continue;
|
|
762
|
+
outline.push({
|
|
763
|
+
kind: 'enum',
|
|
764
|
+
name: name.text,
|
|
765
|
+
range: {
|
|
766
|
+
startLine: actualNode.startPosition.row + 1,
|
|
767
|
+
endLine: actualNode.endPosition.row + 1,
|
|
768
|
+
},
|
|
769
|
+
exported: isExported || exportedNames.has(name.text),
|
|
770
|
+
});
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
case 'lexical_declaration':
|
|
774
|
+
case 'variable_declaration': {
|
|
775
|
+
// const/let/var declarations
|
|
776
|
+
for (const decl of actualNode.descendantsOfType('variable_declarator')) {
|
|
777
|
+
const varName = decl.childForFieldName('name');
|
|
778
|
+
if (!varName || varName.type !== 'identifier')
|
|
779
|
+
continue;
|
|
780
|
+
// Check if it's an arrow function
|
|
781
|
+
const value = decl.childForFieldName('value');
|
|
782
|
+
const isArrowFn = value?.type === 'arrow_function';
|
|
783
|
+
outline.push({
|
|
784
|
+
kind: isArrowFn ? 'function' : 'variable',
|
|
785
|
+
name: varName.text,
|
|
786
|
+
signature: isArrowFn ? extractFunctionSignatureJsTs(value) : undefined,
|
|
787
|
+
range: {
|
|
788
|
+
startLine: decl.startPosition.row + 1,
|
|
789
|
+
endLine: decl.endPosition.row + 1,
|
|
790
|
+
},
|
|
791
|
+
exported: isExported || exportedNames.has(varName.text),
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return outline;
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Extract function signature from Python function definition.
|
|
802
|
+
*/
|
|
803
|
+
function extractFunctionSignaturePython(node) {
|
|
804
|
+
const params = node.childForFieldName('parameters');
|
|
805
|
+
const returnType = node.childForFieldName('return_type');
|
|
806
|
+
if (!params)
|
|
807
|
+
return undefined;
|
|
808
|
+
let sig = params.text;
|
|
809
|
+
if (returnType) {
|
|
810
|
+
sig += ` -> ${returnType.text}`;
|
|
811
|
+
}
|
|
812
|
+
return sig;
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Extract class methods for Python.
|
|
816
|
+
*/
|
|
817
|
+
function extractClassMethodsPython(classNode) {
|
|
818
|
+
const methods = [];
|
|
819
|
+
const body = classNode.childForFieldName('body');
|
|
820
|
+
if (!body)
|
|
821
|
+
return methods;
|
|
822
|
+
for (const child of body.namedChildren) {
|
|
823
|
+
if (child.type === 'function_definition') {
|
|
824
|
+
const name = child.childForFieldName('name');
|
|
825
|
+
if (!name)
|
|
826
|
+
continue;
|
|
827
|
+
const methodName = name.text;
|
|
828
|
+
// Skip private methods (starting with __) except __init__
|
|
829
|
+
const isPrivate = methodName.startsWith('_') && methodName !== '__init__';
|
|
830
|
+
methods.push({
|
|
831
|
+
kind: 'method',
|
|
832
|
+
name: methodName,
|
|
833
|
+
signature: extractFunctionSignaturePython(child),
|
|
834
|
+
range: {
|
|
835
|
+
startLine: child.startPosition.row + 1,
|
|
836
|
+
endLine: child.endPosition.row + 1,
|
|
837
|
+
},
|
|
838
|
+
exported: !isPrivate,
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return methods;
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Extract outline from Python files.
|
|
846
|
+
*/
|
|
847
|
+
function extractOutlinePython(root) {
|
|
848
|
+
const outline = [];
|
|
849
|
+
// Check __all__ for explicit exports
|
|
850
|
+
const exportedNames = new Set();
|
|
851
|
+
for (const asn of root.descendantsOfType(['assignment', 'assignment_statement'])) {
|
|
852
|
+
const left = asn.childForFieldName('left') ?? asn.namedChild(0);
|
|
853
|
+
if (!left || left.text !== '__all__')
|
|
854
|
+
continue;
|
|
855
|
+
const right = asn.childForFieldName('right') ?? asn.namedChild(asn.namedChildCount - 1);
|
|
856
|
+
if (!right)
|
|
857
|
+
continue;
|
|
858
|
+
// Extract names from list/tuple
|
|
859
|
+
for (const s of right.descendantsOfType('string')) {
|
|
860
|
+
const inner = s.text.replace(/^['"]|['"]$/g, '');
|
|
861
|
+
if (inner)
|
|
862
|
+
exportedNames.add(inner);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const hasExplicitAll = exportedNames.size > 0;
|
|
866
|
+
for (const child of root.namedChildren) {
|
|
867
|
+
switch (child.type) {
|
|
868
|
+
case 'function_definition': {
|
|
869
|
+
const name = child.childForFieldName('name');
|
|
870
|
+
if (!name)
|
|
871
|
+
continue;
|
|
872
|
+
const funcName = name.text;
|
|
873
|
+
// Public if: in __all__, or doesn't start with _ (when no __all__)
|
|
874
|
+
const isPublic = hasExplicitAll
|
|
875
|
+
? exportedNames.has(funcName)
|
|
876
|
+
: !funcName.startsWith('_');
|
|
877
|
+
outline.push({
|
|
878
|
+
kind: 'function',
|
|
879
|
+
name: funcName,
|
|
880
|
+
signature: extractFunctionSignaturePython(child),
|
|
881
|
+
range: {
|
|
882
|
+
startLine: child.startPosition.row + 1,
|
|
883
|
+
endLine: child.endPosition.row + 1,
|
|
884
|
+
},
|
|
885
|
+
exported: isPublic,
|
|
886
|
+
});
|
|
887
|
+
break;
|
|
888
|
+
}
|
|
889
|
+
case 'class_definition': {
|
|
890
|
+
const name = child.childForFieldName('name');
|
|
891
|
+
if (!name)
|
|
892
|
+
continue;
|
|
893
|
+
const className = name.text;
|
|
894
|
+
const isPublic = hasExplicitAll
|
|
895
|
+
? exportedNames.has(className)
|
|
896
|
+
: !className.startsWith('_');
|
|
897
|
+
const methods = extractClassMethodsPython(child);
|
|
898
|
+
outline.push({
|
|
899
|
+
kind: 'class',
|
|
900
|
+
name: className,
|
|
901
|
+
range: {
|
|
902
|
+
startLine: child.startPosition.row + 1,
|
|
903
|
+
endLine: child.endPosition.row + 1,
|
|
904
|
+
},
|
|
905
|
+
exported: isPublic,
|
|
906
|
+
children: methods.length > 0 ? methods : undefined,
|
|
907
|
+
});
|
|
908
|
+
break;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return outline;
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Extract outline from Go files.
|
|
916
|
+
*/
|
|
917
|
+
function extractOutlineGo(root) {
|
|
918
|
+
const outline = [];
|
|
919
|
+
for (const child of root.namedChildren) {
|
|
920
|
+
switch (child.type) {
|
|
921
|
+
case 'function_declaration': {
|
|
922
|
+
const name = child.childForFieldName('name');
|
|
923
|
+
if (!name)
|
|
924
|
+
continue;
|
|
925
|
+
const funcName = name.text;
|
|
926
|
+
// Go: exported if starts with uppercase
|
|
927
|
+
const isExported = /^[A-Z]/.test(funcName);
|
|
928
|
+
// Extract signature
|
|
929
|
+
const params = child.childForFieldName('parameters');
|
|
930
|
+
const result = child.childForFieldName('result');
|
|
931
|
+
let sig = params?.text || '()';
|
|
932
|
+
if (result)
|
|
933
|
+
sig += ` ${result.text}`;
|
|
934
|
+
outline.push({
|
|
935
|
+
kind: 'function',
|
|
936
|
+
name: funcName,
|
|
937
|
+
signature: sig,
|
|
938
|
+
range: {
|
|
939
|
+
startLine: child.startPosition.row + 1,
|
|
940
|
+
endLine: child.endPosition.row + 1,
|
|
941
|
+
},
|
|
942
|
+
exported: isExported,
|
|
943
|
+
});
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
946
|
+
case 'method_declaration': {
|
|
947
|
+
const name = child.childForFieldName('name');
|
|
948
|
+
if (!name)
|
|
949
|
+
continue;
|
|
950
|
+
const methodName = name.text;
|
|
951
|
+
const isExported = /^[A-Z]/.test(methodName);
|
|
952
|
+
// Get receiver type
|
|
953
|
+
const receiver = child.childForFieldName('receiver');
|
|
954
|
+
const params = child.childForFieldName('parameters');
|
|
955
|
+
const result = child.childForFieldName('result');
|
|
956
|
+
let sig = params?.text || '()';
|
|
957
|
+
if (result)
|
|
958
|
+
sig += ` ${result.text}`;
|
|
959
|
+
if (receiver)
|
|
960
|
+
sig = `${receiver.text} ${sig}`;
|
|
961
|
+
outline.push({
|
|
962
|
+
kind: 'method',
|
|
963
|
+
name: methodName,
|
|
964
|
+
signature: sig,
|
|
965
|
+
range: {
|
|
966
|
+
startLine: child.startPosition.row + 1,
|
|
967
|
+
endLine: child.endPosition.row + 1,
|
|
968
|
+
},
|
|
969
|
+
exported: isExported,
|
|
970
|
+
});
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
973
|
+
case 'type_declaration': {
|
|
974
|
+
// type Foo struct { ... } or type Bar interface { ... }
|
|
975
|
+
for (const spec of child.descendantsOfType('type_spec')) {
|
|
976
|
+
const name = spec.childForFieldName('name');
|
|
977
|
+
if (!name)
|
|
978
|
+
continue;
|
|
979
|
+
const typeName = name.text;
|
|
980
|
+
const isExported = /^[A-Z]/.test(typeName);
|
|
981
|
+
// Determine kind based on type definition
|
|
982
|
+
const typeNode = spec.childForFieldName('type');
|
|
983
|
+
let kind = 'type';
|
|
984
|
+
if (typeNode?.type === 'struct_type')
|
|
985
|
+
kind = 'class'; // Treat struct as class
|
|
986
|
+
else if (typeNode?.type === 'interface_type')
|
|
987
|
+
kind = 'interface';
|
|
988
|
+
outline.push({
|
|
989
|
+
kind,
|
|
990
|
+
name: typeName,
|
|
991
|
+
range: {
|
|
992
|
+
startLine: spec.startPosition.row + 1,
|
|
993
|
+
endLine: spec.endPosition.row + 1,
|
|
994
|
+
},
|
|
995
|
+
exported: isExported,
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
break;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return outline;
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Extract outline from Rust files.
|
|
1006
|
+
*/
|
|
1007
|
+
function extractOutlineRust(root) {
|
|
1008
|
+
const outline = [];
|
|
1009
|
+
for (const child of root.namedChildren) {
|
|
1010
|
+
switch (child.type) {
|
|
1011
|
+
case 'function_item': {
|
|
1012
|
+
const name = child.childForFieldName('name');
|
|
1013
|
+
if (!name)
|
|
1014
|
+
continue;
|
|
1015
|
+
const funcName = name.text;
|
|
1016
|
+
const isExported = hasRustPubVisibility(child);
|
|
1017
|
+
// Extract signature
|
|
1018
|
+
const params = child.childForFieldName('parameters');
|
|
1019
|
+
const returnType = child.childForFieldName('return_type');
|
|
1020
|
+
let sig = params?.text || '()';
|
|
1021
|
+
if (returnType)
|
|
1022
|
+
sig += ` -> ${returnType.text}`;
|
|
1023
|
+
outline.push({
|
|
1024
|
+
kind: 'function',
|
|
1025
|
+
name: funcName,
|
|
1026
|
+
signature: sig,
|
|
1027
|
+
range: {
|
|
1028
|
+
startLine: child.startPosition.row + 1,
|
|
1029
|
+
endLine: child.endPosition.row + 1,
|
|
1030
|
+
},
|
|
1031
|
+
exported: isExported,
|
|
1032
|
+
});
|
|
1033
|
+
break;
|
|
1034
|
+
}
|
|
1035
|
+
case 'struct_item': {
|
|
1036
|
+
const name = child.childForFieldName('name');
|
|
1037
|
+
if (!name)
|
|
1038
|
+
continue;
|
|
1039
|
+
const structName = name.text;
|
|
1040
|
+
const isExported = hasRustPubVisibility(child);
|
|
1041
|
+
outline.push({
|
|
1042
|
+
kind: 'class', // Treat struct as class
|
|
1043
|
+
name: structName,
|
|
1044
|
+
range: {
|
|
1045
|
+
startLine: child.startPosition.row + 1,
|
|
1046
|
+
endLine: child.endPosition.row + 1,
|
|
1047
|
+
},
|
|
1048
|
+
exported: isExported,
|
|
1049
|
+
});
|
|
1050
|
+
break;
|
|
1051
|
+
}
|
|
1052
|
+
case 'enum_item': {
|
|
1053
|
+
const name = child.childForFieldName('name');
|
|
1054
|
+
if (!name)
|
|
1055
|
+
continue;
|
|
1056
|
+
const enumName = name.text;
|
|
1057
|
+
const isExported = hasRustPubVisibility(child);
|
|
1058
|
+
outline.push({
|
|
1059
|
+
kind: 'enum',
|
|
1060
|
+
name: enumName,
|
|
1061
|
+
range: {
|
|
1062
|
+
startLine: child.startPosition.row + 1,
|
|
1063
|
+
endLine: child.endPosition.row + 1,
|
|
1064
|
+
},
|
|
1065
|
+
exported: isExported,
|
|
1066
|
+
});
|
|
1067
|
+
break;
|
|
1068
|
+
}
|
|
1069
|
+
case 'trait_item': {
|
|
1070
|
+
const name = child.childForFieldName('name');
|
|
1071
|
+
if (!name)
|
|
1072
|
+
continue;
|
|
1073
|
+
const traitName = name.text;
|
|
1074
|
+
const isExported = hasRustPubVisibility(child);
|
|
1075
|
+
outline.push({
|
|
1076
|
+
kind: 'interface', // Treat trait as interface
|
|
1077
|
+
name: traitName,
|
|
1078
|
+
range: {
|
|
1079
|
+
startLine: child.startPosition.row + 1,
|
|
1080
|
+
endLine: child.endPosition.row + 1,
|
|
1081
|
+
},
|
|
1082
|
+
exported: isExported,
|
|
1083
|
+
});
|
|
1084
|
+
break;
|
|
1085
|
+
}
|
|
1086
|
+
case 'type_item': {
|
|
1087
|
+
const name = child.childForFieldName('name');
|
|
1088
|
+
if (!name)
|
|
1089
|
+
continue;
|
|
1090
|
+
const typeName = name.text;
|
|
1091
|
+
const isExported = hasRustPubVisibility(child);
|
|
1092
|
+
outline.push({
|
|
1093
|
+
kind: 'type',
|
|
1094
|
+
name: typeName,
|
|
1095
|
+
range: {
|
|
1096
|
+
startLine: child.startPosition.row + 1,
|
|
1097
|
+
endLine: child.endPosition.row + 1,
|
|
1098
|
+
},
|
|
1099
|
+
exported: isExported,
|
|
1100
|
+
});
|
|
1101
|
+
break;
|
|
1102
|
+
}
|
|
1103
|
+
case 'impl_item': {
|
|
1104
|
+
// Extract methods from impl blocks
|
|
1105
|
+
const typeNode = child.childForFieldName('type');
|
|
1106
|
+
const typeName = typeNode?.text || 'impl';
|
|
1107
|
+
// Find all function_items inside the impl
|
|
1108
|
+
const body = child.childForFieldName('body');
|
|
1109
|
+
if (!body)
|
|
1110
|
+
continue;
|
|
1111
|
+
const methods = [];
|
|
1112
|
+
for (const item of body.namedChildren) {
|
|
1113
|
+
if (item.type === 'function_item') {
|
|
1114
|
+
const name = item.childForFieldName('name');
|
|
1115
|
+
if (!name)
|
|
1116
|
+
continue;
|
|
1117
|
+
const methodName = name.text;
|
|
1118
|
+
const isExported = hasRustPubVisibility(item);
|
|
1119
|
+
const params = item.childForFieldName('parameters');
|
|
1120
|
+
const returnType = item.childForFieldName('return_type');
|
|
1121
|
+
let sig = params?.text || '()';
|
|
1122
|
+
if (returnType)
|
|
1123
|
+
sig += ` -> ${returnType.text}`;
|
|
1124
|
+
methods.push({
|
|
1125
|
+
kind: 'method',
|
|
1126
|
+
name: methodName,
|
|
1127
|
+
signature: sig,
|
|
1128
|
+
range: {
|
|
1129
|
+
startLine: item.startPosition.row + 1,
|
|
1130
|
+
endLine: item.endPosition.row + 1,
|
|
1131
|
+
},
|
|
1132
|
+
exported: isExported,
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if (methods.length > 0) {
|
|
1137
|
+
outline.push({
|
|
1138
|
+
kind: 'class', // impl block as a class-like container
|
|
1139
|
+
name: `impl ${typeName}`,
|
|
1140
|
+
range: {
|
|
1141
|
+
startLine: child.startPosition.row + 1,
|
|
1142
|
+
endLine: child.endPosition.row + 1,
|
|
1143
|
+
},
|
|
1144
|
+
exported: true, // impl blocks are always "public" in terms of structure
|
|
1145
|
+
children: methods,
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
break;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
return outline;
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Extract symbol outline from a source file.
|
|
1156
|
+
* Returns null if the file type is not supported.
|
|
1157
|
+
*/
|
|
1158
|
+
export async function extractOutlineWasm(filePath, normalizedContent) {
|
|
1159
|
+
const lang = languageForFile(filePath);
|
|
1160
|
+
if (!lang)
|
|
1161
|
+
return null;
|
|
1162
|
+
// Supported languages: JS/TS, Python, Go, Rust
|
|
1163
|
+
if (!['javascript', 'typescript', 'tsx', 'python', 'go', 'rust'].includes(lang)) {
|
|
1164
|
+
return null;
|
|
1165
|
+
}
|
|
1166
|
+
const language = await loadLanguage(lang);
|
|
1167
|
+
const parser = new Parser();
|
|
1168
|
+
try {
|
|
1169
|
+
parser.setLanguage(language);
|
|
1170
|
+
const tree = parser.parse(normalizedContent);
|
|
1171
|
+
if (!tree)
|
|
1172
|
+
return { language: lang, outline: [] };
|
|
1173
|
+
try {
|
|
1174
|
+
const root = tree.rootNode;
|
|
1175
|
+
let outline;
|
|
1176
|
+
switch (lang) {
|
|
1177
|
+
case 'python':
|
|
1178
|
+
outline = extractOutlinePython(root);
|
|
1179
|
+
break;
|
|
1180
|
+
case 'go':
|
|
1181
|
+
outline = extractOutlineGo(root);
|
|
1182
|
+
break;
|
|
1183
|
+
case 'rust':
|
|
1184
|
+
outline = extractOutlineRust(root);
|
|
1185
|
+
break;
|
|
1186
|
+
default:
|
|
1187
|
+
outline = extractOutlineJsTs(root, lang);
|
|
1188
|
+
}
|
|
1189
|
+
return { language: lang, outline };
|
|
1190
|
+
}
|
|
1191
|
+
finally {
|
|
1192
|
+
tree.delete();
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
finally {
|
|
1196
|
+
parser.delete();
|
|
1197
|
+
}
|
|
1198
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -21,6 +21,7 @@ import { suggestTestedByTraces } from './trace/suggest.js';
|
|
|
21
21
|
import { generateRepoTree, formatTreeResult } from './bundle/tree.js';
|
|
22
22
|
import { buildDeepAnalysis, detectTestInfo } from './analysis/deep.js';
|
|
23
23
|
import { validateReport } from './analysis/validate.js';
|
|
24
|
+
import { extractOutlineWasm } from './ast/treeSitter.js';
|
|
24
25
|
// RFC v2: New aggregation tools
|
|
25
26
|
import { ReadFilesInputSchema, createReadFilesHandler, readFilesToolDescription } from './tools/readFiles.js';
|
|
26
27
|
import { SearchAndReadInputSchema, createSearchAndReadHandler, searchAndReadToolDescription } from './tools/searchAndRead.js';
|
|
@@ -147,7 +148,7 @@ export async function startServer() {
|
|
|
147
148
|
startHttpServer(cfg);
|
|
148
149
|
const server = new McpServer({
|
|
149
150
|
name: 'preflight-mcp',
|
|
150
|
-
version: '0.5.
|
|
151
|
+
version: '0.5.3',
|
|
151
152
|
description: 'Create evidence-based preflight bundles for repositories (docs + code) with SQLite FTS search.',
|
|
152
153
|
}, {
|
|
153
154
|
capabilities: {
|
|
@@ -390,6 +391,16 @@ export async function startServer() {
|
|
|
390
391
|
includeDepsGraph: z.boolean().optional().default(false).describe('Include deps/dependency-graph.json in batch mode.'),
|
|
391
392
|
withLineNumbers: z.boolean().optional().default(false).describe('If true, prefix each line with line number in "N|" format for evidence citation.'),
|
|
392
393
|
ranges: z.array(z.string()).optional().describe('Line ranges to read, e.g. ["20-80", "100-120"]. Each range is "start-end" (1-indexed, inclusive). If omitted, reads entire file.'),
|
|
394
|
+
// NEW: outline mode for code structure extraction
|
|
395
|
+
outline: z.boolean().optional().default(false).describe('If true, return symbol outline instead of file content. ' +
|
|
396
|
+
'Returns function/class/method/interface/type/enum with line ranges. ' +
|
|
397
|
+
'Saves tokens by showing code structure without full content. ' +
|
|
398
|
+
'Supports: .ts, .tsx, .js, .jsx, .py, .go, .rs files.'),
|
|
399
|
+
// NEW: symbol-based reading
|
|
400
|
+
symbol: z.string().optional().describe('Read a specific symbol (function/class/method) by name. ' +
|
|
401
|
+
'Format: "functionName" or "ClassName" or "ClassName.methodName". ' +
|
|
402
|
+
'Automatically locates and returns the symbol\'s code with context. ' +
|
|
403
|
+
'Requires outline-supported file types (.ts, .tsx, .js, .jsx, .py).'),
|
|
393
404
|
},
|
|
394
405
|
outputSchema: {
|
|
395
406
|
bundleId: z.string(),
|
|
@@ -405,6 +416,16 @@ export async function startServer() {
|
|
|
405
416
|
end: z.number(),
|
|
406
417
|
})),
|
|
407
418
|
}).optional(),
|
|
419
|
+
// NEW: outline output
|
|
420
|
+
outline: z.array(z.object({
|
|
421
|
+
kind: z.enum(['function', 'class', 'method', 'interface', 'type', 'enum', 'variable']),
|
|
422
|
+
name: z.string(),
|
|
423
|
+
signature: z.string().optional(),
|
|
424
|
+
range: z.object({ startLine: z.number(), endLine: z.number() }),
|
|
425
|
+
exported: z.boolean(),
|
|
426
|
+
children: z.array(z.any()).optional(),
|
|
427
|
+
})).optional().describe('Symbol outline (when outline=true).'),
|
|
428
|
+
language: z.string().optional().describe('Detected language (when outline=true).'),
|
|
408
429
|
},
|
|
409
430
|
annotations: {
|
|
410
431
|
readOnlyHint: true,
|
|
@@ -460,6 +481,115 @@ export async function startServer() {
|
|
|
460
481
|
if (args.file) {
|
|
461
482
|
const absPath = safeJoin(bundleRoot, args.file);
|
|
462
483
|
const rawContent = await fs.readFile(absPath, 'utf8');
|
|
484
|
+
const normalizedContent = rawContent.replace(/\r\n/g, '\n');
|
|
485
|
+
// NEW: Outline mode - extract symbol structure
|
|
486
|
+
if (args.outline) {
|
|
487
|
+
const outlineResult = await extractOutlineWasm(args.file, normalizedContent);
|
|
488
|
+
if (!outlineResult) {
|
|
489
|
+
// Unsupported file type
|
|
490
|
+
return {
|
|
491
|
+
content: [{ type: 'text', text: `[${args.file}] Outline not supported for this file type. Supported: .ts, .tsx, .js, .jsx` }],
|
|
492
|
+
structuredContent: {
|
|
493
|
+
bundleId: args.bundleId,
|
|
494
|
+
file: args.file,
|
|
495
|
+
outline: null,
|
|
496
|
+
language: null,
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
// Format outline as readable text
|
|
501
|
+
const formatOutlineText = (symbols, indent = '') => {
|
|
502
|
+
const lines = [];
|
|
503
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
504
|
+
const sym = symbols[i];
|
|
505
|
+
const isLast = i === symbols.length - 1;
|
|
506
|
+
const prefix = indent + (isLast ? '└── ' : '├── ');
|
|
507
|
+
const exportMark = sym.exported ? '⚡' : '';
|
|
508
|
+
const sig = sym.signature ? sym.signature : '';
|
|
509
|
+
lines.push(`${prefix}${exportMark}${sym.kind} ${sym.name}${sig} :${sym.range.startLine}-${sym.range.endLine}`);
|
|
510
|
+
if (sym.children && sym.children.length > 0) {
|
|
511
|
+
const childIndent = indent + (isLast ? ' ' : '│ ');
|
|
512
|
+
lines.push(...formatOutlineText(sym.children, childIndent));
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return lines;
|
|
516
|
+
};
|
|
517
|
+
const outlineText = formatOutlineText(outlineResult.outline);
|
|
518
|
+
const totalSymbols = outlineResult.outline.length;
|
|
519
|
+
const header = `[${args.file}] Outline (${totalSymbols} top-level symbols, ${outlineResult.language}):\n`;
|
|
520
|
+
return {
|
|
521
|
+
content: [{ type: 'text', text: header + outlineText.join('\n') }],
|
|
522
|
+
structuredContent: {
|
|
523
|
+
bundleId: args.bundleId,
|
|
524
|
+
file: args.file,
|
|
525
|
+
outline: outlineResult.outline,
|
|
526
|
+
language: outlineResult.language,
|
|
527
|
+
},
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
// NEW: Symbol-based reading - locate and read a specific symbol
|
|
531
|
+
if (args.symbol) {
|
|
532
|
+
const outlineResult = await extractOutlineWasm(args.file, normalizedContent);
|
|
533
|
+
if (!outlineResult) {
|
|
534
|
+
return {
|
|
535
|
+
content: [{ type: 'text', text: `[${args.file}] Symbol lookup not supported for this file type. Supported: .ts, .tsx, .js, .jsx, .py` }],
|
|
536
|
+
structuredContent: { bundleId: args.bundleId, file: args.file, error: 'unsupported_file_type' },
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
// Parse symbol query: "funcName" or "ClassName.methodName"
|
|
540
|
+
const parts = args.symbol.split('.');
|
|
541
|
+
const targetName = parts[0];
|
|
542
|
+
const methodName = parts[1];
|
|
543
|
+
// Find the symbol in outline
|
|
544
|
+
let foundSymbol;
|
|
545
|
+
let foundIn = 'top';
|
|
546
|
+
for (const sym of outlineResult.outline) {
|
|
547
|
+
if (sym.name === targetName) {
|
|
548
|
+
if (methodName && sym.children) {
|
|
549
|
+
// Looking for a method inside this class
|
|
550
|
+
const method = sym.children.find(c => c.name === methodName);
|
|
551
|
+
if (method) {
|
|
552
|
+
foundSymbol = method;
|
|
553
|
+
foundIn = 'child';
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
foundSymbol = sym;
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (!foundSymbol) {
|
|
564
|
+
// Symbol not found - provide helpful error with available symbols
|
|
565
|
+
const available = outlineResult.outline.map(s => {
|
|
566
|
+
if (s.children && s.children.length > 0) {
|
|
567
|
+
return `${s.name} (${s.kind}, methods: ${s.children.map(c => c.name).join(', ')})`;
|
|
568
|
+
}
|
|
569
|
+
return `${s.name} (${s.kind})`;
|
|
570
|
+
}).join(', ');
|
|
571
|
+
return {
|
|
572
|
+
content: [{ type: 'text', text: `[${args.file}] Symbol "${args.symbol}" not found.\n\nAvailable symbols: ${available}` }],
|
|
573
|
+
structuredContent: { bundleId: args.bundleId, file: args.file, error: 'symbol_not_found', available: outlineResult.outline.map(s => s.name) },
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
// Read the symbol's code (with 2 lines of context before)
|
|
577
|
+
const contextLines = 2;
|
|
578
|
+
const startLine = Math.max(1, foundSymbol.range.startLine - contextLines);
|
|
579
|
+
const endLine = foundSymbol.range.endLine;
|
|
580
|
+
const { content, lineInfo } = formatContent(rawContent, true, [{ start: startLine, end: endLine }]);
|
|
581
|
+
const header = `[${args.file}:${startLine}-${endLine}] ${foundSymbol.kind} ${foundSymbol.name}${foundSymbol.signature || ''}\n\n`;
|
|
582
|
+
return {
|
|
583
|
+
content: [{ type: 'text', text: header + content }],
|
|
584
|
+
structuredContent: {
|
|
585
|
+
bundleId: args.bundleId,
|
|
586
|
+
file: args.file,
|
|
587
|
+
symbol: foundSymbol,
|
|
588
|
+
content,
|
|
589
|
+
lineInfo,
|
|
590
|
+
},
|
|
591
|
+
};
|
|
592
|
+
}
|
|
463
593
|
// Parse ranges if provided
|
|
464
594
|
let parsedRanges;
|
|
465
595
|
if (args.ranges && args.ranges.length > 0) {
|