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.
Files changed (58) hide show
  1. fastgraph_mcp-0.2.0/.github/workflows/ci.yml +25 -0
  2. fastgraph_mcp-0.2.0/.gitignore +17 -0
  3. fastgraph_mcp-0.2.0/LICENSE +21 -0
  4. fastgraph_mcp-0.2.0/PKG-INFO +157 -0
  5. fastgraph_mcp-0.2.0/README.md +128 -0
  6. fastgraph_mcp-0.2.0/bench/bench.py +125 -0
  7. fastgraph_mcp-0.2.0/fastgraph/__init__.py +3 -0
  8. fastgraph_mcp-0.2.0/fastgraph/__main__.py +7 -0
  9. fastgraph_mcp-0.2.0/fastgraph/cli.py +106 -0
  10. fastgraph_mcp-0.2.0/fastgraph/config.py +206 -0
  11. fastgraph_mcp-0.2.0/fastgraph/db.py +563 -0
  12. fastgraph_mcp-0.2.0/fastgraph/gitutil.py +135 -0
  13. fastgraph_mcp-0.2.0/fastgraph/graph.py +1918 -0
  14. fastgraph_mcp-0.2.0/fastgraph/index.py +965 -0
  15. fastgraph_mcp-0.2.0/fastgraph/parsers/__init__.py +28 -0
  16. fastgraph_mcp-0.2.0/fastgraph/parsers/base.py +65 -0
  17. fastgraph_mcp-0.2.0/fastgraph/parsers/cpp.py +134 -0
  18. fastgraph_mcp-0.2.0/fastgraph/parsers/frontend.py +103 -0
  19. fastgraph_mcp-0.2.0/fastgraph/parsers/go.py +170 -0
  20. fastgraph_mcp-0.2.0/fastgraph/parsers/java.py +162 -0
  21. fastgraph_mcp-0.2.0/fastgraph/parsers/python.py +232 -0
  22. fastgraph_mcp-0.2.0/fastgraph/parsers/registry.py +28 -0
  23. fastgraph_mcp-0.2.0/fastgraph/parsers/rust.py +104 -0
  24. fastgraph_mcp-0.2.0/fastgraph/parsers/typescript.py +336 -0
  25. fastgraph_mcp-0.2.0/fastgraph/parsers/util.py +103 -0
  26. fastgraph_mcp-0.2.0/fastgraph/parsers/wxml.py +55 -0
  27. fastgraph_mcp-0.2.0/fastgraph/search.py +202 -0
  28. fastgraph_mcp-0.2.0/fastgraph/server.py +257 -0
  29. fastgraph_mcp-0.2.0/fastgraph/tools.py +617 -0
  30. fastgraph_mcp-0.2.0/pyproject.toml +44 -0
  31. fastgraph_mcp-0.2.0/tests/test_alias_deps.py +66 -0
  32. fastgraph_mcp-0.2.0/tests/test_alias_imports.py +36 -0
  33. fastgraph_mcp-0.2.0/tests/test_analysis_tools.py +128 -0
  34. fastgraph_mcp-0.2.0/tests/test_call_graph_honesty.py +274 -0
  35. fastgraph_mcp-0.2.0/tests/test_class_level_graph.py +50 -0
  36. fastgraph_mcp-0.2.0/tests/test_content_search.py +35 -0
  37. fastgraph_mcp-0.2.0/tests/test_fastgraphignore.py +120 -0
  38. fastgraph_mcp-0.2.0/tests/test_framework_registration.py +94 -0
  39. fastgraph_mcp-0.2.0/tests/test_frontend.py +106 -0
  40. fastgraph_mcp-0.2.0/tests/test_frontend_template_refs.py +50 -0
  41. fastgraph_mcp-0.2.0/tests/test_gitutil.py +44 -0
  42. fastgraph_mcp-0.2.0/tests/test_home_guard.py +23 -0
  43. fastgraph_mcp-0.2.0/tests/test_inner_variable_callees.py +43 -0
  44. fastgraph_mcp-0.2.0/tests/test_java_callers.py +220 -0
  45. fastgraph_mcp-0.2.0/tests/test_lifecycle_unused.py +69 -0
  46. fastgraph_mcp-0.2.0/tests/test_live_test_fixes.py +274 -0
  47. fastgraph_mcp-0.2.0/tests/test_multilang_coverage.py +552 -0
  48. fastgraph_mcp-0.2.0/tests/test_pipeline.py +459 -0
  49. fastgraph_mcp-0.2.0/tests/test_read_source.py +172 -0
  50. fastgraph_mcp-0.2.0/tests/test_resolver_upgrades.py +330 -0
  51. fastgraph_mcp-0.2.0/tests/test_review_fixes.py +311 -0
  52. fastgraph_mcp-0.2.0/tests/test_round2_regressions.py +390 -0
  53. fastgraph_mcp-0.2.0/tests/test_schema_tables.py +76 -0
  54. fastgraph_mcp-0.2.0/tests/test_stress_regressions.py +375 -0
  55. fastgraph_mcp-0.2.0/tests/test_symbol_name_paths.py +178 -0
  56. fastgraph_mcp-0.2.0/tests/test_tool_schema.py +55 -0
  57. fastgraph_mcp-0.2.0/tests/test_unused_symbols_vue.py +41 -0
  58. 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,17 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .fastgraph/
7
+ .serena/
8
+ .venv/
9
+ venv/
10
+ .pytest_cache/
11
+ 参考项目/
12
+ # test scratch directories
13
+ tests/work/
14
+ tests/work_*/
15
+ tests/*_work/
16
+ tests/*_work*/
17
+ tests/*_analysis/
@@ -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,3 @@
1
+ """FastGraph-MCP: lightweight code intelligence for coding agents."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,7 @@
1
+ # mypy: disable-error-code=import-untyped
2
+ """Entry point for `python -m fastgraph`."""
3
+
4
+ from fastgraph.cli import main
5
+
6
+ if __name__ == "__main__":
7
+ 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())