fastgraph-mcp 0.2.0__tar.gz
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.
- fastgraph_mcp-0.2.0/.github/workflows/ci.yml +25 -0
- fastgraph_mcp-0.2.0/.gitignore +17 -0
- fastgraph_mcp-0.2.0/LICENSE +21 -0
- fastgraph_mcp-0.2.0/PKG-INFO +157 -0
- fastgraph_mcp-0.2.0/README.md +128 -0
- fastgraph_mcp-0.2.0/bench/bench.py +125 -0
- fastgraph_mcp-0.2.0/fastgraph/__init__.py +3 -0
- fastgraph_mcp-0.2.0/fastgraph/__main__.py +7 -0
- fastgraph_mcp-0.2.0/fastgraph/cli.py +106 -0
- fastgraph_mcp-0.2.0/fastgraph/config.py +206 -0
- fastgraph_mcp-0.2.0/fastgraph/db.py +563 -0
- fastgraph_mcp-0.2.0/fastgraph/gitutil.py +135 -0
- fastgraph_mcp-0.2.0/fastgraph/graph.py +1918 -0
- fastgraph_mcp-0.2.0/fastgraph/index.py +965 -0
- fastgraph_mcp-0.2.0/fastgraph/parsers/__init__.py +28 -0
- fastgraph_mcp-0.2.0/fastgraph/parsers/base.py +65 -0
- fastgraph_mcp-0.2.0/fastgraph/parsers/cpp.py +134 -0
- fastgraph_mcp-0.2.0/fastgraph/parsers/frontend.py +103 -0
- fastgraph_mcp-0.2.0/fastgraph/parsers/go.py +170 -0
- fastgraph_mcp-0.2.0/fastgraph/parsers/java.py +162 -0
- fastgraph_mcp-0.2.0/fastgraph/parsers/python.py +232 -0
- fastgraph_mcp-0.2.0/fastgraph/parsers/registry.py +28 -0
- fastgraph_mcp-0.2.0/fastgraph/parsers/rust.py +104 -0
- fastgraph_mcp-0.2.0/fastgraph/parsers/typescript.py +336 -0
- fastgraph_mcp-0.2.0/fastgraph/parsers/util.py +103 -0
- fastgraph_mcp-0.2.0/fastgraph/parsers/wxml.py +55 -0
- fastgraph_mcp-0.2.0/fastgraph/search.py +202 -0
- fastgraph_mcp-0.2.0/fastgraph/server.py +257 -0
- fastgraph_mcp-0.2.0/fastgraph/tools.py +617 -0
- fastgraph_mcp-0.2.0/pyproject.toml +44 -0
- fastgraph_mcp-0.2.0/tests/test_alias_deps.py +66 -0
- fastgraph_mcp-0.2.0/tests/test_alias_imports.py +36 -0
- fastgraph_mcp-0.2.0/tests/test_analysis_tools.py +128 -0
- fastgraph_mcp-0.2.0/tests/test_call_graph_honesty.py +274 -0
- fastgraph_mcp-0.2.0/tests/test_class_level_graph.py +50 -0
- fastgraph_mcp-0.2.0/tests/test_content_search.py +35 -0
- fastgraph_mcp-0.2.0/tests/test_fastgraphignore.py +120 -0
- fastgraph_mcp-0.2.0/tests/test_framework_registration.py +94 -0
- fastgraph_mcp-0.2.0/tests/test_frontend.py +106 -0
- fastgraph_mcp-0.2.0/tests/test_frontend_template_refs.py +50 -0
- fastgraph_mcp-0.2.0/tests/test_gitutil.py +44 -0
- fastgraph_mcp-0.2.0/tests/test_home_guard.py +23 -0
- fastgraph_mcp-0.2.0/tests/test_inner_variable_callees.py +43 -0
- fastgraph_mcp-0.2.0/tests/test_java_callers.py +220 -0
- fastgraph_mcp-0.2.0/tests/test_lifecycle_unused.py +69 -0
- fastgraph_mcp-0.2.0/tests/test_live_test_fixes.py +274 -0
- fastgraph_mcp-0.2.0/tests/test_multilang_coverage.py +552 -0
- fastgraph_mcp-0.2.0/tests/test_pipeline.py +459 -0
- fastgraph_mcp-0.2.0/tests/test_read_source.py +172 -0
- fastgraph_mcp-0.2.0/tests/test_resolver_upgrades.py +330 -0
- fastgraph_mcp-0.2.0/tests/test_review_fixes.py +311 -0
- fastgraph_mcp-0.2.0/tests/test_round2_regressions.py +390 -0
- fastgraph_mcp-0.2.0/tests/test_schema_tables.py +76 -0
- fastgraph_mcp-0.2.0/tests/test_stress_regressions.py +375 -0
- fastgraph_mcp-0.2.0/tests/test_symbol_name_paths.py +178 -0
- fastgraph_mcp-0.2.0/tests/test_tool_schema.py +55 -0
- fastgraph_mcp-0.2.0/tests/test_unused_symbols_vue.py +41 -0
- fastgraph_mcp-0.2.0/tests/test_wxml_refs.py +47 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ${{ matrix.os }}
|
|
10
|
+
strategy:
|
|
11
|
+
fail-fast: false
|
|
12
|
+
matrix:
|
|
13
|
+
os: [ubuntu-latest, windows-latest]
|
|
14
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- name: Install
|
|
21
|
+
run: |
|
|
22
|
+
python -m pip install --upgrade pip
|
|
23
|
+
python -m pip install -e ".[dev]"
|
|
24
|
+
- name: Test
|
|
25
|
+
run: python -m pytest tests/ -q
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FastGraph-MCP contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fastgraph-mcp
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Lightweight real-time code intelligence MCP: AST + incremental index + code graph, no embeddings, low memory & low context.
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Requires-Dist: mcp>=1.11
|
|
17
|
+
Requires-Dist: tree-sitter-c>=0.23
|
|
18
|
+
Requires-Dist: tree-sitter-cpp>=0.23
|
|
19
|
+
Requires-Dist: tree-sitter-go>=0.23
|
|
20
|
+
Requires-Dist: tree-sitter-java>=0.23
|
|
21
|
+
Requires-Dist: tree-sitter-javascript>=0.23
|
|
22
|
+
Requires-Dist: tree-sitter-python>=0.23
|
|
23
|
+
Requires-Dist: tree-sitter-rust>=0.23
|
|
24
|
+
Requires-Dist: tree-sitter-typescript>=0.23
|
|
25
|
+
Requires-Dist: tree-sitter<0.28,>=0.24
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# FastGraph-MCP
|
|
31
|
+
|
|
32
|
+
轻量级代码智能 [MCP](https://modelcontextprotocol.io) 服务器:AST + 增量索引 + 调用图。无 embedding、无图数据库、无 LSP,低内存、低 context。
|
|
33
|
+
|
|
34
|
+
定位是**代码导航层**:代码在哪、谁调用谁、改了影响谁、刚改了什么——补 grep(分不清定义与调用点)和 LSP(无全局调用图/影响分析)的短板。读改代码、重构、diagnostics 交给编辑器/LSP 工具。
|
|
35
|
+
|
|
36
|
+
## 特性
|
|
37
|
+
|
|
38
|
+
- **快**:首次索引中型项目数秒~十几秒;之后增量更新只重解析改动文件,典型查询 <100ms
|
|
39
|
+
- **轻**:无 embedding 模型、无图数据库、无 LSP;常驻内存 <50MB
|
|
40
|
+
- **省 context**:所有工具只返回 `file / symbol / line / relation`,不返回文件正文(仅 `read_file` / `symbol_body` 例外,且有硬上限)
|
|
41
|
+
- **诚实**:调用图是名字解析的近似而非类型推断;解析不了的边显式报 `unresolved_incoming`/`unresolved_outgoing`,绝不猜、绝不把"空列表"伪装成"没人用"
|
|
42
|
+
|
|
43
|
+
## 安装
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
cd /path/to/FastGraph-mcp
|
|
47
|
+
python -m pip install . # 日常使用(装完源码目录可删)
|
|
48
|
+
python -m pip install -e ".[dev]" # 开发者(改动即时生效)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
要求 Python ≥ 3.11。
|
|
52
|
+
|
|
53
|
+
## 快速开始
|
|
54
|
+
|
|
55
|
+
**1. 配置 MCP**(零配置:不写 `--root`、不写 `cwd`,打开哪个项目就索引哪个项目)
|
|
56
|
+
|
|
57
|
+
Claude Code(`~/.claude.json` 或项目 `.mcp.json`):
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"mcpServers": {
|
|
62
|
+
"fastgraph": {
|
|
63
|
+
"command": "python",
|
|
64
|
+
"args": ["-m", "fastgraph"]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
OpenCode(`opencode.json`):
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"mcp": {
|
|
75
|
+
"fastgraph": {
|
|
76
|
+
"type": "local",
|
|
77
|
+
"enabled": true,
|
|
78
|
+
"command": ["python", "-m", "fastgraph"]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**2. 使用**:打开任意项目,调用任意工具(推荐先 `project_overview`)即自动建索引,之后增量更新。
|
|
85
|
+
|
|
86
|
+
桌面客户端(Claude Desktop 等,以固定 cwd 启动 MCP 进程)先调一次 `activate_project(root="/abs/path")` 激活项目;换项目再调一次。要固定分析某个目录,启动参数加 `--root D:/work/my-project`(或环境变量 `FASTGRAPH_ROOT`)。
|
|
87
|
+
|
|
88
|
+
> Windows 注意:`python` 不要用微软商店别名(用 `where python` 确认);JSON 路径推荐写正斜杠 `D:/work/project`。
|
|
89
|
+
|
|
90
|
+
## 让模型主动使用
|
|
91
|
+
|
|
92
|
+
MCP 工具对模型是可选的——不引导,模型默认用自己顺手的 grep/read。最有效的一步:把下面这段粘贴进项目的 `CLAUDE.md` / `AGENTS.md`(服务端 instructions 与工具描述已内置同样的决策表):
|
|
93
|
+
|
|
94
|
+
```md
|
|
95
|
+
## 代码导航:用 FastGraph MCP,不要先用 grep
|
|
96
|
+
- 找代码在哪 / 找用法:先 code_search
|
|
97
|
+
- 打开不熟悉的文件前:先 file_symbols(path) 看结构
|
|
98
|
+
- 回答"谁调用了 X":用 find_callers(结果为空但带 unresolved_incoming>0 表示有调用者未解析,不能当无人使用)
|
|
99
|
+
- 改函数/类之前:先 impact_analysis(symbol) 看影响面
|
|
100
|
+
- 改 import 前:file_deps(path);查架构:module_cycles()
|
|
101
|
+
- 只读一个符号:symbol_body(name);整文件才用 read_file
|
|
102
|
+
- 改完代码:changed_context() 复查波及范围
|
|
103
|
+
- 进陌生仓库:先 project_overview()
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## 工具(13 个)
|
|
107
|
+
|
|
108
|
+
| 任务 | 工具 |
|
|
109
|
+
| --- | --- |
|
|
110
|
+
| 桌面端激活项目 | `activate_project(root)` |
|
|
111
|
+
| 陌生仓库看全局 | `project_overview()` |
|
|
112
|
+
| 按关键词/自然语言定位代码 | `code_search(query, kind?, limit?)` |
|
|
113
|
+
| 一个符号是什么 | `symbol_info(symbol)` |
|
|
114
|
+
| 文件结构(先看结构再读正文) | `file_symbols(path)` |
|
|
115
|
+
| 读文件 / 行区间 | `read_file(path, start_line?, end_line?)` |
|
|
116
|
+
| 只读一个符号的源码 | `symbol_body(symbol)` |
|
|
117
|
+
| 谁调用它(`depth≥2` 传递) | `find_callers(symbol, depth?)` |
|
|
118
|
+
| 它调用谁 | `find_callees(symbol, depth?)` |
|
|
119
|
+
| 改动前看影响面 | `impact_analysis(symbol)` |
|
|
120
|
+
| import 了什么 / 被谁 import | `file_deps(path)` |
|
|
121
|
+
| 文件间 import 环(架构健康) | `module_cycles()` |
|
|
122
|
+
| 改完看变更波及(支持 `base="main"` 对比整个分支) | `changed_context(base?)` |
|
|
123
|
+
|
|
124
|
+
推荐节奏:`project_overview` → `code_search` 定位 → `file_symbols`/`symbol_body` 读 → `impact_analysis` 改前 → `changed_context` 改后。
|
|
125
|
+
|
|
126
|
+
所有工具都接受可选 `root=` 参数做单次跨项目查询(按 LRU 缓存 8 个项目)。输出行号全部 1-based。另有 resource `fastgraph://overview` 与 prompt `fastgraph-workflow` 可选配。
|
|
127
|
+
|
|
128
|
+
## 支持语言
|
|
129
|
+
|
|
130
|
+
Python、TypeScript/TSX、JavaScript、Vue、Svelte、Go、Rust、Java、C/C++(`.wxml` 模板引用亦支持)。
|
|
131
|
+
|
|
132
|
+
依赖图按各语言 import 写法解析,含 tsconfig paths / vite alias / uni-app `@` 别名、Go `go.mod` 模块根、Rust crate 根、目录入口(`index.*` / `__init__.*`)。
|
|
133
|
+
|
|
134
|
+
## 调用图的边界(重要)
|
|
135
|
+
|
|
136
|
+
真实仓库上约 21%~40% 的调用边能唯一连到符号,其余要么指向外部库,要么接收者类型无法静态确定——后者**不连边、不猜**,而是计数报出:
|
|
137
|
+
|
|
138
|
+
- `find_callers` 每条结果带 `via`:`resolved`(确证的调用边)或 `text`(同名文本折叠的猜测)
|
|
139
|
+
- 出现 `unresolved_incoming` / `unresolved_outgoing` 字段时,**空列表 ≠ 没有调用者**,按"未知"处理,不能据此判定可删
|
|
140
|
+
- `self.x()`、接口实现、回调注册、动态派发不会连边——宁可漏,不可错
|
|
141
|
+
|
|
142
|
+
## 开发
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
python -m pytest tests/ # 测试
|
|
146
|
+
python bench/bench.py <项目路径> # 性能基准(冷索引/增量/查询时延)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## 路线图
|
|
150
|
+
|
|
151
|
+
- [x] Phase 1-3.5:MCP Server、调用图、影响分析、git 变更感知、多语言依赖解析
|
|
152
|
+
- [ ] Phase 4:可选 BM25/embedding 语义搜索
|
|
153
|
+
- [ ] Phase 5:多项目 workspace
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# FastGraph-MCP
|
|
2
|
+
|
|
3
|
+
轻量级代码智能 [MCP](https://modelcontextprotocol.io) 服务器:AST + 增量索引 + 调用图。无 embedding、无图数据库、无 LSP,低内存、低 context。
|
|
4
|
+
|
|
5
|
+
定位是**代码导航层**:代码在哪、谁调用谁、改了影响谁、刚改了什么——补 grep(分不清定义与调用点)和 LSP(无全局调用图/影响分析)的短板。读改代码、重构、diagnostics 交给编辑器/LSP 工具。
|
|
6
|
+
|
|
7
|
+
## 特性
|
|
8
|
+
|
|
9
|
+
- **快**:首次索引中型项目数秒~十几秒;之后增量更新只重解析改动文件,典型查询 <100ms
|
|
10
|
+
- **轻**:无 embedding 模型、无图数据库、无 LSP;常驻内存 <50MB
|
|
11
|
+
- **省 context**:所有工具只返回 `file / symbol / line / relation`,不返回文件正文(仅 `read_file` / `symbol_body` 例外,且有硬上限)
|
|
12
|
+
- **诚实**:调用图是名字解析的近似而非类型推断;解析不了的边显式报 `unresolved_incoming`/`unresolved_outgoing`,绝不猜、绝不把"空列表"伪装成"没人用"
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
cd /path/to/FastGraph-mcp
|
|
18
|
+
python -m pip install . # 日常使用(装完源码目录可删)
|
|
19
|
+
python -m pip install -e ".[dev]" # 开发者(改动即时生效)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
要求 Python ≥ 3.11。
|
|
23
|
+
|
|
24
|
+
## 快速开始
|
|
25
|
+
|
|
26
|
+
**1. 配置 MCP**(零配置:不写 `--root`、不写 `cwd`,打开哪个项目就索引哪个项目)
|
|
27
|
+
|
|
28
|
+
Claude Code(`~/.claude.json` 或项目 `.mcp.json`):
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"mcpServers": {
|
|
33
|
+
"fastgraph": {
|
|
34
|
+
"command": "python",
|
|
35
|
+
"args": ["-m", "fastgraph"]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
OpenCode(`opencode.json`):
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcp": {
|
|
46
|
+
"fastgraph": {
|
|
47
|
+
"type": "local",
|
|
48
|
+
"enabled": true,
|
|
49
|
+
"command": ["python", "-m", "fastgraph"]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**2. 使用**:打开任意项目,调用任意工具(推荐先 `project_overview`)即自动建索引,之后增量更新。
|
|
56
|
+
|
|
57
|
+
桌面客户端(Claude Desktop 等,以固定 cwd 启动 MCP 进程)先调一次 `activate_project(root="/abs/path")` 激活项目;换项目再调一次。要固定分析某个目录,启动参数加 `--root D:/work/my-project`(或环境变量 `FASTGRAPH_ROOT`)。
|
|
58
|
+
|
|
59
|
+
> Windows 注意:`python` 不要用微软商店别名(用 `where python` 确认);JSON 路径推荐写正斜杠 `D:/work/project`。
|
|
60
|
+
|
|
61
|
+
## 让模型主动使用
|
|
62
|
+
|
|
63
|
+
MCP 工具对模型是可选的——不引导,模型默认用自己顺手的 grep/read。最有效的一步:把下面这段粘贴进项目的 `CLAUDE.md` / `AGENTS.md`(服务端 instructions 与工具描述已内置同样的决策表):
|
|
64
|
+
|
|
65
|
+
```md
|
|
66
|
+
## 代码导航:用 FastGraph MCP,不要先用 grep
|
|
67
|
+
- 找代码在哪 / 找用法:先 code_search
|
|
68
|
+
- 打开不熟悉的文件前:先 file_symbols(path) 看结构
|
|
69
|
+
- 回答"谁调用了 X":用 find_callers(结果为空但带 unresolved_incoming>0 表示有调用者未解析,不能当无人使用)
|
|
70
|
+
- 改函数/类之前:先 impact_analysis(symbol) 看影响面
|
|
71
|
+
- 改 import 前:file_deps(path);查架构:module_cycles()
|
|
72
|
+
- 只读一个符号:symbol_body(name);整文件才用 read_file
|
|
73
|
+
- 改完代码:changed_context() 复查波及范围
|
|
74
|
+
- 进陌生仓库:先 project_overview()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## 工具(13 个)
|
|
78
|
+
|
|
79
|
+
| 任务 | 工具 |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| 桌面端激活项目 | `activate_project(root)` |
|
|
82
|
+
| 陌生仓库看全局 | `project_overview()` |
|
|
83
|
+
| 按关键词/自然语言定位代码 | `code_search(query, kind?, limit?)` |
|
|
84
|
+
| 一个符号是什么 | `symbol_info(symbol)` |
|
|
85
|
+
| 文件结构(先看结构再读正文) | `file_symbols(path)` |
|
|
86
|
+
| 读文件 / 行区间 | `read_file(path, start_line?, end_line?)` |
|
|
87
|
+
| 只读一个符号的源码 | `symbol_body(symbol)` |
|
|
88
|
+
| 谁调用它(`depth≥2` 传递) | `find_callers(symbol, depth?)` |
|
|
89
|
+
| 它调用谁 | `find_callees(symbol, depth?)` |
|
|
90
|
+
| 改动前看影响面 | `impact_analysis(symbol)` |
|
|
91
|
+
| import 了什么 / 被谁 import | `file_deps(path)` |
|
|
92
|
+
| 文件间 import 环(架构健康) | `module_cycles()` |
|
|
93
|
+
| 改完看变更波及(支持 `base="main"` 对比整个分支) | `changed_context(base?)` |
|
|
94
|
+
|
|
95
|
+
推荐节奏:`project_overview` → `code_search` 定位 → `file_symbols`/`symbol_body` 读 → `impact_analysis` 改前 → `changed_context` 改后。
|
|
96
|
+
|
|
97
|
+
所有工具都接受可选 `root=` 参数做单次跨项目查询(按 LRU 缓存 8 个项目)。输出行号全部 1-based。另有 resource `fastgraph://overview` 与 prompt `fastgraph-workflow` 可选配。
|
|
98
|
+
|
|
99
|
+
## 支持语言
|
|
100
|
+
|
|
101
|
+
Python、TypeScript/TSX、JavaScript、Vue、Svelte、Go、Rust、Java、C/C++(`.wxml` 模板引用亦支持)。
|
|
102
|
+
|
|
103
|
+
依赖图按各语言 import 写法解析,含 tsconfig paths / vite alias / uni-app `@` 别名、Go `go.mod` 模块根、Rust crate 根、目录入口(`index.*` / `__init__.*`)。
|
|
104
|
+
|
|
105
|
+
## 调用图的边界(重要)
|
|
106
|
+
|
|
107
|
+
真实仓库上约 21%~40% 的调用边能唯一连到符号,其余要么指向外部库,要么接收者类型无法静态确定——后者**不连边、不猜**,而是计数报出:
|
|
108
|
+
|
|
109
|
+
- `find_callers` 每条结果带 `via`:`resolved`(确证的调用边)或 `text`(同名文本折叠的猜测)
|
|
110
|
+
- 出现 `unresolved_incoming` / `unresolved_outgoing` 字段时,**空列表 ≠ 没有调用者**,按"未知"处理,不能据此判定可删
|
|
111
|
+
- `self.x()`、接口实现、回调注册、动态派发不会连边——宁可漏,不可错
|
|
112
|
+
|
|
113
|
+
## 开发
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
python -m pytest tests/ # 测试
|
|
117
|
+
python bench/bench.py <项目路径> # 性能基准(冷索引/增量/查询时延)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## 路线图
|
|
121
|
+
|
|
122
|
+
- [x] Phase 1-3.5:MCP Server、调用图、影响分析、git 变更感知、多语言依赖解析
|
|
123
|
+
- [ ] Phase 4:可选 BM25/embedding 语义搜索
|
|
124
|
+
- [ ] Phase 5:多项目 workspace
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Reproducible benchmark for FastGraph's headline numbers.
|
|
2
|
+
|
|
3
|
+
Measures on a target repo (any project directory):
|
|
4
|
+
1. cold first index (delete .fastgraph, run one tool call)
|
|
5
|
+
2. no-op refresh (query again, nothing changed)
|
|
6
|
+
3. incremental refresh (touch one file's content, query again)
|
|
7
|
+
4. typical query latency (code_search / symbol_info / find_callers / file_deps)
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python bench/bench.py /path/to/project [--keep]
|
|
11
|
+
|
|
12
|
+
`--keep` preserves the built index (default: the pre-existing .fastgraph state
|
|
13
|
+
is restored — the bench never leaves its own index behind unless asked).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import shutil
|
|
20
|
+
import statistics
|
|
21
|
+
import sys
|
|
22
|
+
import time
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from fastgraph.db import DB
|
|
26
|
+
from fastgraph.index import Indexer
|
|
27
|
+
from fastgraph import graph
|
|
28
|
+
from fastgraph.search import code_search
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _timed(fn, *args, **kwargs):
|
|
32
|
+
t0 = time.perf_counter()
|
|
33
|
+
out = fn(*args, **kwargs)
|
|
34
|
+
return (time.perf_counter() - t0) * 1000.0, out
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _query_suite(db: DB, probe_symbol: str | None) -> dict[str, float]:
|
|
38
|
+
"""Latency of one representative call of each query family."""
|
|
39
|
+
res: dict[str, float] = {}
|
|
40
|
+
res["code_search"] = _timed(code_search, db, "config", limit=10)[0]
|
|
41
|
+
res["project_overview"] = _timed(graph.project_overview, db)[0]
|
|
42
|
+
target = probe_symbol or _first_function(db)
|
|
43
|
+
if target:
|
|
44
|
+
res["symbol_info"] = _timed(graph.find_symbols, db, target)[0]
|
|
45
|
+
res["find_callers"] = _timed(graph.find_callers, db, target, limit=30)[0]
|
|
46
|
+
res["impact_analysis"] = _timed(graph.impact_analysis, db, target)[0]
|
|
47
|
+
paths = [r[0] for r in db.conn.execute("SELECT path FROM files LIMIT 1")]
|
|
48
|
+
if paths:
|
|
49
|
+
res["file_deps"] = _timed(graph.module_dependencies, db, paths[0])[0]
|
|
50
|
+
res["module_cycles"] = _timed(graph.module_cycles, db)[0]
|
|
51
|
+
return res
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _first_function(db: DB) -> str | None:
|
|
55
|
+
row = db.conn.execute(
|
|
56
|
+
"SELECT name FROM symbols WHERE kind='function' ORDER BY id LIMIT 1"
|
|
57
|
+
).fetchone()
|
|
58
|
+
return row[0] if row else None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main(argv: list[str] | None = None) -> int:
|
|
62
|
+
ap = argparse.ArgumentParser(description="FastGraph benchmark")
|
|
63
|
+
ap.add_argument("root", type=Path, help="project directory to benchmark")
|
|
64
|
+
ap.add_argument("--keep", action="store_true", help="leave the built index in place")
|
|
65
|
+
args = ap.parse_args(argv)
|
|
66
|
+
root = args.root.resolve()
|
|
67
|
+
if not root.is_dir():
|
|
68
|
+
print(f"error: {root} is not a directory", file=sys.stderr)
|
|
69
|
+
return 1
|
|
70
|
+
|
|
71
|
+
index_dir = root / ".fastgraph"
|
|
72
|
+
backup: Path | None = None
|
|
73
|
+
if index_dir.exists():
|
|
74
|
+
backup = root / ".fastgraph.bench-backup"
|
|
75
|
+
shutil.move(str(index_dir), str(backup))
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
db = DB(root)
|
|
79
|
+
indexer = Indexer(root, db)
|
|
80
|
+
|
|
81
|
+
cold_ms, stats = _timed(indexer.refresh)
|
|
82
|
+
print(f"files={stats.total_files} symbols={stats.total_symbols}")
|
|
83
|
+
print(f"cold first index : {cold_ms:8.1f} ms")
|
|
84
|
+
|
|
85
|
+
noop_ms, _ = _timed(indexer.refresh)
|
|
86
|
+
print(f"no-op refresh : {noop_ms:8.1f} ms")
|
|
87
|
+
|
|
88
|
+
# touch one content change: append a harmless line to a random indexed file
|
|
89
|
+
touched = None
|
|
90
|
+
for (p,) in db.conn.execute("SELECT path FROM files LIMIT 50"):
|
|
91
|
+
f = root / p
|
|
92
|
+
if f.is_file():
|
|
93
|
+
touched = f
|
|
94
|
+
break
|
|
95
|
+
if touched is not None:
|
|
96
|
+
orig = touched.read_bytes()
|
|
97
|
+
touched.write_bytes(orig + b"\n# bench touch\n")
|
|
98
|
+
try:
|
|
99
|
+
inc_ms, _ = _timed(indexer.refresh)
|
|
100
|
+
print(f"incremental (1f) : {inc_ms:8.1f} ms")
|
|
101
|
+
finally:
|
|
102
|
+
touched.write_bytes(orig)
|
|
103
|
+
|
|
104
|
+
# run the query suite several times, report median
|
|
105
|
+
runs: list[dict[str, float]] = []
|
|
106
|
+
for _ in range(5):
|
|
107
|
+
runs.append(_query_suite(db, None))
|
|
108
|
+
print("\nquery latency (median of 5):")
|
|
109
|
+
keys = sorted({k for r in runs for k in r})
|
|
110
|
+
for k in keys:
|
|
111
|
+
vals = [r[k] for r in runs if k in r]
|
|
112
|
+
print(f" {k:<18} {statistics.median(vals):8.1f} ms")
|
|
113
|
+
|
|
114
|
+
db.close()
|
|
115
|
+
if not args.keep:
|
|
116
|
+
shutil.rmtree(index_dir, ignore_errors=True)
|
|
117
|
+
return 0
|
|
118
|
+
finally:
|
|
119
|
+
if backup is not None and not args.keep:
|
|
120
|
+
shutil.rmtree(index_dir, ignore_errors=True)
|
|
121
|
+
shutil.move(str(backup), str(index_dir))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""CLI entrypoint: fastgraph [--root <path>] (stdio MCP server).
|
|
2
|
+
|
|
3
|
+
Without --root, the project root is auto-detected: walk up from the current
|
|
4
|
+
directory to the nearest git root (repo boundary); non-git locations fall
|
|
5
|
+
back to the current directory. MCP clients that launch the server with their
|
|
6
|
+
working directory set to the opened folder therefore index that folder -
|
|
7
|
+
open any project, no config per project.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def detect_root(start: Path) -> Path:
|
|
18
|
+
"""Nearest git root above ``start``, or ``start`` itself if none found.
|
|
19
|
+
|
|
20
|
+
``.git`` may be a directory (normal repo) or a file (gitfile in
|
|
21
|
+
submodules / worktrees), so ``.exists()`` covers both.
|
|
22
|
+
"""
|
|
23
|
+
cur = start
|
|
24
|
+
while True:
|
|
25
|
+
if (cur / ".git").exists():
|
|
26
|
+
return cur
|
|
27
|
+
parent = cur.parent
|
|
28
|
+
if parent == cur: # filesystem root: nothing found above
|
|
29
|
+
return start
|
|
30
|
+
cur = parent
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _writable(path: Path) -> bool:
|
|
34
|
+
"""True if a `.fastgraph` index dir can be created under ``path``."""
|
|
35
|
+
try:
|
|
36
|
+
probe = path / ".fastgraph_probe"
|
|
37
|
+
probe.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
probe.rmdir()
|
|
39
|
+
return True
|
|
40
|
+
except OSError:
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def resolve_root(start: Path) -> Path:
|
|
45
|
+
"""Root to index: nearest git root above ``start`` if writable.
|
|
46
|
+
|
|
47
|
+
Falls back through ancestors to the first writable dir (desktop apps
|
|
48
|
+
launch stdio servers with cwd=C:\\Windows\\System32 etc., which is not
|
|
49
|
+
writable); final fallback is the user home dir. Prints a hint to stderr
|
|
50
|
+
whenever the detected root is not `start` itself.
|
|
51
|
+
"""
|
|
52
|
+
from os import getenv
|
|
53
|
+
|
|
54
|
+
home = Path(getenv("USERPROFILE") or Path.home())
|
|
55
|
+
root = detect_root(start)
|
|
56
|
+
if not _writable(root):
|
|
57
|
+
# Desktop apps launch stdio servers with cwd=C:\Windows\System32 etc.
|
|
58
|
+
# - not writable. Prefer the user home over climbing to a system root.
|
|
59
|
+
print(
|
|
60
|
+
f"notice: {root} is not writable, falling back to {home}",
|
|
61
|
+
file=sys.stderr,
|
|
62
|
+
)
|
|
63
|
+
root = home
|
|
64
|
+
elif root != start:
|
|
65
|
+
print(f"notice: indexing git root {root} (cwd: {start})", file=sys.stderr)
|
|
66
|
+
return root
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def main(argv: list[str] | None = None) -> int:
|
|
70
|
+
from os import getenv
|
|
71
|
+
|
|
72
|
+
parser = argparse.ArgumentParser(
|
|
73
|
+
prog="fastgraph",
|
|
74
|
+
description="FastGraph-MCP stdio server (default root: auto-detected from cwd)",
|
|
75
|
+
)
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
"--root",
|
|
78
|
+
type=Path,
|
|
79
|
+
default=None,
|
|
80
|
+
help="project root to index (default: $FASTGRAPH_ROOT, else nearest git root above cwd, else cwd)",
|
|
81
|
+
)
|
|
82
|
+
args = parser.parse_args(argv)
|
|
83
|
+
|
|
84
|
+
explicit = args.root or (
|
|
85
|
+
Path(getenv("FASTGRAPH_ROOT")) if getenv("FASTGRAPH_ROOT") else None
|
|
86
|
+
)
|
|
87
|
+
root = (
|
|
88
|
+
explicit.resolve()
|
|
89
|
+
if explicit is not None
|
|
90
|
+
else resolve_root(Path.cwd())
|
|
91
|
+
)
|
|
92
|
+
if not root.is_dir():
|
|
93
|
+
print(f"error: {root} is not a directory", file=sys.stderr)
|
|
94
|
+
return 1
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
from fastgraph.server import run_stdio
|
|
98
|
+
|
|
99
|
+
run_stdio(root)
|
|
100
|
+
except KeyboardInterrupt:
|
|
101
|
+
return 0
|
|
102
|
+
return 0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
raise SystemExit(main())
|