内容
# ast-lens-mcp
> 一个MCP服务器,为AI代理提供**结构化理解**,使其能够查询TypeScript/JavaScript代码库的结构,而不是将整个文件读取到其上下文窗口中。
`ast-lens-mcp`使用Babel AST工具链解析您的项目,并通过[模型上下文协议](https://modelcontextprotocol.io)公开十二种聚焦的只读工具。将其指向项目根目录,LLM客户端(Claude Desktop、Cursor或任何支持MCP的客户端)可以提出精确的结构化问题:*这里导出了哪些符号?这个函数在哪里被调用?哪些函数过于复杂?这个模块导入了什么?哪些导出或文件看起来已死?这个包导出了什么?* — 所有这些都不需要将整个文件加载到模型中。
它完全在您的**本地文件**上运行。无需API密钥,无需网络调用,无需凭证。
- **使用技术:** TypeScript(严格模式),官方[`@modelcontextprotocol/sdk`](https://www.npmjs.com/package/@modelcontextprotocol/sdk),[`@babel/parser`](https://babeljs.io/docs/babel-parser) / [`@babel/traverse`](https://babeljs.io/docs/babel-traverse) / `@babel/types`用于AST分析,[`zod`](https://zod.dev)用于工具输入模式,[`vitest`](https://vitest.dev)用于测试。
- **传输方式:** stdio。 **运行时:** Node 18+。 **模块系统:** ESM。
- **分析语言:** `.ts`, `.tsx`, `.js`, `.jsx`, `.mts`, `.cts`, `.mjs`, `.cjs`。 `node_modules`和构建输出自动被忽略。
---
## 为什么存在这个项目
编码代理浪费了很多上下文重新阅读文件,只是为了回答结构化问题(“这个符号是否存在?”,“它在哪里被使用?”,“这个类的形状是什么?)。这些问题有**精确**的答案,这些答案来自AST,而不是近似读取。`ast-lens-mcp`将它们转化为廉价的确定性工具调用,返回小型结构化JSON — 从而为模型的实际推理留出更多上下文。
它被设计为**语法**,而不是类型感知:它解析,但不进行类型检查。这使其保持快速,依赖轻量,并能够在无需整个程序构建的情况下分析文件。一些图工具读取简单的`tsconfig`路径别名以更好地进行模块解析,但名称/引用工具保持基于名称。`find_references`因此是一个精确的**基于名称**搜索,按语法角色分类,而不是类型解析的重命名索引 — 请参见下面的注释。
---
## 安装
```bash
# 从npm(发布后)
npm install -g ast-lens-mcp
# 或在不安装的情况下运行
npx ast-lens-mcp /path/to/your/project
```
从源码:
```bash
git clone <your-repo-url> ast-lens-mcp
cd ast-lens-mcp
npm install
npm run build
node dist/index.js /path/to/your/project # 在stdio上启动
```
分析工具的项目根目录按以下顺序解析:
1. 位置CLI参数或`--root <path>`标志,
2. `AST_LENS_PROJECT_ROOT`环境变量,
3. 当前工作目录。
---
## MCP客户端配置
### Claude Desktop
在`claude_desktop_config.json`中添加一个条目(macOS:`~/Library/Application Support/Claude/claude_desktop_config.json`):
```json
{
"mcpServers": {
"ast-lens": {
"command": "npx",
"args": ["-y", "ast-lens-mcp", "/absolute/path/to/your/project"]
}
}
}
```
或,如果全局安装/从源码安装:
```json
{
"mcpServers": {
"ast-lens": {
"command": "node",
"args": ["/absolute/path/to/ast-lens-mcp/dist/index.js"],
"env": {
"AST_LENS_PROJECT_ROOT": "/absolute/path/to/your/project"
}
}
}
}
```
### 通用`mcp.json`(Cursor,VS Code MCP等)
```json
{
"mcpServers": {
"ast-lens": {
"command": "npx",
"args": ["-y", "ast-lens-mcp", "${workspaceFolder}"]
}
}
}
```
传递给工具的所有路径都相对于项目根目录(或绝对路径,只要它们在根目录内)。目录遍历到根目录外部被拒绝,解析到根目录外部位置的符号链接也被拒绝 — 服务器在读取任何文件之前验证实际(符号链接解析)路径。
---
## 工具
每个工具都是只读的(`readOnlyHint: true`,`openWorldHint: false`),使用严格的zod模式验证输入,返回人类可读文本和机器可读的`structuredContent`,支持`response_format: "json" | "markdown"`,并且在输入错误时不会抛出异常 — 解析失败以结构化的`parseErrors`形式返回。
| # | 工具 | 用途 |
|---|---|---|
| 1 | `list_symbols` | 跨文件、文件夹或通配符的顶级和导出符号 |
| 2 | `get_file_outline` | 单个文件的类/接口/枚举结构 |
| 3 | `find_references` | 具有语法上下文的基于名称的标识符引用 |
| 4 | `search_ast` | 结构化AST查询和常见代码气味 |
| 5 | `analyze_complexity` | 每个函数的圈复杂度(McCabe)和代码行数 |
| 6 | `summarize_module` | 文件的导入、导出和依赖项 |
| 7 | `find_unused_exports` | 未在定义文件外部引用的导出符号 |
| 8 | `call_graph` | 函数到函数的调用关系 |
| 9 | `import_graph` | 解析的模块导入、再导出和动态导入边 |
| 10 | `detect_circular_deps` | 模块图中的循环依赖 |
| 11 | `find_dead_files` | 无法从包或索引入口点到达的源文件 |
| 12 | `api_surface` | 从包或入口文件可达的公共符号 |
| 13 | `compare_implementations` | 两个候选解决方案与结构化信号的比较(偏好排序/评分支持) |
### 1. `list_symbols`
跨文件、目录或通配符的所有顶级/导出符号(函数、类、接口、类型、枚举、const/let/var)。
```jsonc
// 输入
{ "target": "src/models.ts", "kinds": ["function", "class"] }
// 输出(缩略)
{
"totalSymbols": 3,
"fileCount": 1,
"files": [
{
"file": "src/models.ts",
"symbols": [
{ "name": "nextId", "kind": "function", "exported": true,
"exportKind": "named", "span": { "start": {"line":21,"column":8}, "end": {"line":24,"column":2} } },
{ "name": "UserService", "kind": "class", "exported": true, "exportKind": "named",
"span": { "start": {"line":26,"column":8}, "end": {"line":44,"column":2} } }
]
}
],
"parseErrors": []
}
```
选项:`kinds`(过滤器),`exportedOnly`,`ignore`(额外通配符),`response_format`。
### 2. `get_file_outline`
单个文件的分层大纲:类及其方法/属性,接口及其成员,枚举及其成员。
```jsonc
// 输入
{ "file": "src/models.ts" }
// 输出(缩略)
{
"file": "src/models.ts",
"symbolCount": 8,
"outline": [
{ "name": "User", "kind": "interface", "exported": true,
"children": [
{ "name": "id", "kind": "property", "span": { "start": {"line":4,"column":3}, ... } },
{ "name": "getDisplayName", "kind": "method", "span": { ... } }
] },
{ "name": "UserService", "kind": "class", "exported": true,
"children": [
{ "name": "create", "kind": "method", "static": true, "span": { ... } },
{ "name": "count", "kind": "getter", "span": { ... } },
{ "name": "findById", "kind": "method", "async": true, "span": { ... } }
] }
]
}
```
### 3. `find_references`
项目中每个标识符名称的引用,具有位置、源代码片段和语法上下文分类(`call` / `import` / `declaration` / `type` / `jsx` / `reference`)。
```jsonc
// 输入
{ "name": "UserService", "target": "src" }
// 输出(缩略)
{
"name": "UserService",
"total": 6,
"count": 6,
"references": [
{ "file": "src/models.ts", "context": "declaration",
"snippet": "export class UserService {", "span": { "start": {"line":26,"column":14}, ... } },
{ "file": "src/models.ts", "context": "call",
"snippet": "return new UserService();", "span": { ... } },
{ "file": "src/widget.tsx", "context": "import",
"snippet": "import { UserService } from \"./models\";", "span": { ... } }
],
"byContext": { "declaration": 1, "type": 1, "call": 1, "reference": 2, "import": 1 },
"parseErrors": [ { "file": "src/broken.ts", "message": "Unexpected keyword 'const'. (3:2)", "position": {"line":3,"column":3} } ]
}
```
> **注意:** 这是一个快速的基于名称的搜索(无完整类型解析),因此来自不同作用域/模块的相同名称都被报告。使用代码片段和上下文进行消歧义,并使用`byContext`直方图进行计数,例如仅计算调用站点。选项:`target`(默认为整个项目),`includeDeclarations`,`ignore`,`limit`,`response_format`。
### 4. `search_ast`
AST上的结构化查询 — 比文本搜索更精确。提供了一组精选的命名查询和通用节点类型逃生阀:
| 查询 | 查找 |
|---|---|
| `calls_to` | 对特定调用者(设置`callee`,例如`"console.log"`或`"fetch"`)的调用 |
| `console_usage` | 任何`console.*`调用 |
| `any_usage` | TypeScript `any`类型注解 |
| `non_null_assertion` | TypeScript `!`非空断言 |
| `await_in_loop` | `await`在`for`/`while`循环内(顺序等待性能气味) |
| `empty_catch` | 带有空体的`catch`子句(吞噬错误) |
| `todo_fixme` | 评论中的`TODO` / `FIXME` / `HACK` / `XXX`标记 |
| `ts_ignore` | `@ts-ignore` / `@ts-expect-error` / `@ts-nocheck`抑制注释 |
| `node_type` | 通用:匹配任何Babel节点类型(设置`nodeType`,例如`"TryStatement"`) |
```jsonc
// 输入
{ "query": "await_in_loop", "target": "src/smelly.ts" }
// 输出
{
"query": "await_in_loop",
"total": 1,
"matches": [
{ "file": "src/smelly.ts", "nodeType": "AwaitExpression",
"snippet": "const result = await fetchValue(item); // await in loop",
"span": { "start": {"line":8,"column":20}, "end": {"line":8,"column":42} } }
],
"parseErrors": []
}
```
### 5. `analyze_complexity`
每个函数的圈复杂度(McCabe:`1 + decision points` — `if`,循环,每个非默认`case`,`catch`,三元运算符,`&&`/`||`/`??`,以及可选链接)加上代码行数,带有限标志。嵌套函数独立测量。
```jsonc
// 输入
{ "target": "src/smelly.ts", "threshold": 5, "flaggedOnly": true }
// 输出
{
"threshold": 5,
"totalFunctions": 4,
"flaggedCount": 1,
"maxComplexity": 6,
"averageComplexity": 2.75,
"files": [
{ "file": "src/smelly.ts",
"functions": [
{ "name": "classify", "kind": "function", "complexity": 6, "loc": 16, "params": 1,
"overThreshold": true, "span": { "start": {"line":23,"column":8}, ... } }
] }
]
}
```
选项:`threshold`,`flaggedOnly`,`sortBy`(`complexity` | `loc` | `location`),`ignore`,`limit`,`response_format`。
### 6. `summarize_module`
一个文件的导入、导出和依赖项 — 包括静态导入、再导出、动态`import()`调用和`require()`调用。依赖项分为本地(相对/绝对路径)和外部(bare包说明符)。
```jsonc
// 输入
{ "file": "src/barrel.ts" }
// 输出(缩略)
{
"file": "src/barrel.ts",
"imports": [
{ "source": "node:fs/promises", "typeOnly": false, "bindings": [ { "local": "readFile", "imported": "readFile" } ] },
{ "source": "./models", "typeOnly": true, "bindings": [ { "local": "User", "imported": "User" } ] },
{ "source": "./models", "typeOnly": false, "bindings": [ { "local": "models", "imported": "*" } ] },
{ "source": "./models", "typeOnly": false, "bindings": [ { "local": "defaultService", "imported": "default" } ] }
],
"exports": [
{ "name": "Role", "kind": "named", "source": "./models", "typeOnly": false },
{ "name": "*", "kind": "named", "source": "./widget", "typeOnly": false },
{ "name": "loadConfig", "kind": "named", "typeOnly": false }
],
"dependencies": ["./dynamic-mod", "./legacy-cjs", "./models", "./widget", "node:fs/promises"],
"localDependencies": ["./dynamic-mod", "./legacy-cjs", "./models", "./widget"],
"externalDependencies": ["node:fs/promises"],
"counts": { "imports": 4, "exports": 6, "dependencies": 5 }
}
```
### 7. `find_unused_exports`
候选死公共表面:从扫描范围内另一个文件永远不会引用的导出符号。包入口点和索引式入口点默认被视为故意公开API。
```jsonc
// 输入
{ "target": "src", "entryPoints": ["src/index.ts"] }
// 输出(缩略)
{
"scanned": 27,
"totalExports": 71,
"total": 4,
"unused": [
{ "file": "src/internal.ts", "name": "debugOnly", "exportKind": "named", "reexport": false }
],
"entryPoints": ["src/index.ts"],
"parseErrors": []
}
```
这是基于名称的,而不是类型解析的。它旨在生成一个审查列表,而不是自动删除列表。
### 8. `call_graph`
文件、文件夹或通配符的函数到函数调用关系。节点是类似函数的定义;边是从封闭函数到解析后的调用者的调用站点,当有明确的名称匹配时。
```jsonc
// 输入
{ "target": "src/core", "includeExternalCalls": false }
// 输出(缩略)
{
"nodeCount": 42,
"edgeCount": 67,
"unresolvedCallees": 12,
"nodes": [
{ "id": "src/core/parser.ts#parseFile@28", "name": "parseFile", "file": "src/core/parser.ts", "kind": "function" }
],
"edges": [
{ "from": "src/core/parser.ts#parseFile@28", "callee": "parse", "to": "src/core/parser.ts#parse@11", "file": "src/core/parser.ts" }
],
"parseErrors": []
}
```
设置`includeExternalCalls: true`时,请将调用库、内置函数或未解析方法保留在边缘列表中,并将`to: null`。
### 9. `import_graph`
解析的模块级导入、再导出、动态导入和`require()`调用。相对路径、目录索引、TS-ESM `.js`说明符和简单的`tsconfig`路径别名(如果可能)在作用域内映射到具体文件。
```jsonc
// 输入
{ "target": "src", "includeExternal": true }
// 输出(缩略)
{
"nodeCount": 27,
"edgeCount": 97,
"internalEdges": 97,
"externalCount": 11,
"unresolvedEdges": 0,
"nodes": [{ "id": "src/index.ts", "entry": true }],
"edges": [
{ "from": "src/server.ts", "specifier": "./tools/listSymbols.js", "to": "src/tools/listSymbols.ts", "kind": "import", "resolution": "internal", "symbols": ["registerListSymbols"] }
],
"parseErrors": []
}
```
作用域很重要:如果导入文件在`target`之外,则报告为未解析的边缘。
### 10. `detect_circular_deps`
模块图中的循环导入依赖项,包括自导入。它基于路径,来自与`import_graph`相同的解析图模型。
```jsonc
// 输入
{ "target": "src", "includeDynamic": true, "includeTypeOnly": true }
// 输出(缩略)
{
"scanned": 27,
"edgeCount": 97,
"cycleCount": 0,
"hasCycles": false,
"cycles": [],
"unresolvedLocal": [],
"parseErrors": []
}
```
使用`includeTypeOnly: false`时,类型循环对于您要询问的问题来说可能是噪音。
### 11. `find_dead_files`
候选孤儿模块:源文件无法从`package.json`入口点或索引式入口点到达。动态导入、测试入口点、生成的文件和资产都可以改变解释,因此将结果视为分类列表。
```jsonc
// 输入
{ "target": "src", "entryPoints": ["src/index.ts"] }
// 输出(缩略)
{
"scanned": 27,
"entryFileCount": 1,
"reachableCount": 27,
"total": 0,
"deadFiles": [],
"entryPoints": ["src/index.ts"],
"parseErrors": []
}
```
动态导入、测试入口点、生成的文件和资产都可以改变解释,因此将结果视为分类列表。
### 12. `api_surface`
从包目录或单个入口文件可达的公共API。它会递归跟踪再导出,并返回每个公共符号及其最佳努力源签名。
```jsonc
// 输入
{ "target": "src/server.ts", "includeMembers": true }
// 输出(缩略)
{
"target": "src/server.ts",
"packageEntryPoints": [],
"entries": [
{
"entry": "src/server.ts",
"symbols": [
{ "name": "createServer", "kind": "function", "exportKind": "named", "signature": "function createServer(opts: CreateServerOptions): McpServer", "declaredIn": "src/server.ts" }
],
"unresolved": []
}
],
"totalSymbols": 1,
"parseErrors": []
}
```
传递`target: "."`表示包根目录,或显式文件,如`src/server.ts`,当您只想要一个入口文件。
### 13. `compare_implementations`
两个候选解决方案与结构化信号的比较(偏好排序/评分支持)
### 13. `compare_implementations`
比较两个实现相同功能的实现,例如一个任务的两个候选解决方案,基于客观的、从抽象语法树(AST)中导出的结构信号,返回一个并排的表格和一个透明的偏好推荐。每个信号都是一个确定性的计数(圈复杂度、空 `catch` 块、`console.*` 调用、TypeScript `any` 注解、非空断言、TODO/FIXME 标记、参数计数、错误处理)。推荐是一个可审计的按维度计数的汇总,权重包含在输出中。
这是一个支持做出“哪个解决方案更好,为什么”的判断的决策工具,而不是正确性的裁决——输出的`注意事项`明确说明了这一点,指标也不检测逻辑错误、缺失需求、性能或安全问题。请运行候选方案自己的测试以获取真实结果。
```jsonc
// 输入
{ "left": "solutions/a.ts", "right": "solutions/b.ts", "leftLabel": "a", "rightLabel": "b" }
// 输出(简化)
{
"left": { "label": "a", "metrics": { "averageComplexity": 1.67, "emptyCatch": 0, "anyAnnotations": 0, "maxParams": 2 } },
"right": { "label": "b", "metrics": { "averageComplexity": 8, "emptyCatch": 1, "anyAnnotations": 6, "maxParams": 5 } },
"dimensions": [ { "name": "averageComplexity", "weight": 2, "leftValue": 1.67, "rightValue": 8, "winner": "left" } ],
"score": { "left": 10, "right": 0 },
"recommendation": "prefer_left",
"rationale": [ "a 的平均复杂度较低(1.67 vs 8)。", "a 的空 catch 块较少(0 vs 1)。" ],
"caveats": [ "这些是客观的结构信号,而不是正确性或行为判断——请运行候选方案自己的测试以获取真实结果。" ]
}
```
`recommendation` 是以下之一:`prefer_left`、`prefer_right`、`comparable`(无可衡量的结构优势)或 `insufficient_signal`(两侧均未定义函数)。
---
## 架构
```
src/
index.ts # 二进制入口——解析项目根目录,启动标准输入/输出传输
server.ts # 构建 McpServer 并注册所有工具(与传输无关)
core/ # 可重用的抽象语法树层
parser.ts # Babel 解析 + 基于修改时间的解析缓存(永不抛出)
files.ts # glob/目录/文件发现,node_modules 忽略,路径遍历沙箱
traverse.ts # 共享遍历助手 + 圈复杂度引擎
extract.ts # 符号/大纲/模块摘要提取
entryPoints.ts # 包入口和 TypeScript-ESM 源解析助手
importGraph.ts # 已解析的导入/导出图构建器
moduleGraph.ts # 依赖循环图助手
signature.ts # 尽力而为的源签名提取
context.ts # 服务器上下文:共享根目录 + 缓存 + 批量加载器
response.ts # JSON/Markdown 格式化,字符限制保护,错误结果
types.ts # 共享结构化输出类型
tools/ # 每个工具一个文件,每个工具导出 register<Tool>(server, ctx)
listSymbols.ts getFileOutline.ts findReferences.ts
searchAst.ts analyzeComplexity.ts summarizeModule.ts
findUnusedExports.ts callGraph.ts importGraph.ts
detectCircularDeps.ts findDeadFiles.ts apiSurface.ts
shared.ts # 共享 zod 模式片段
test/
core.test.ts # parser,cache,path-safety,complexity
tools.test.ts # 每个工具,通过内存 MCP 客户端进行端到端测试
fixtures/sample-project/ # 小型、现实的测试夹具(包括一个故意损坏的文件)
scripts/
smoke.mjs # 启动构建的服务器,通过标准输入/输出进行真实测试,并调用工具
```
设计笔记:
- **构造鲁棒性**。一个坏文件永远不会导致调用崩溃:解析器返回一个结构化结果,批量工具将失败收集到 `parseErrors` 中。对于期望单个文件的工具,当提供目录或 glob 时,会返回可行的错误。
- **沙箱**。工具输入被限制在项目根目录内;任何解析到项目根目录外部的路径(包括通过指向树外的符号链接)都会被拒绝(`PathEscapeError`),在读取任何文件之前。
- **上下文友好**。响应带有 25k 字符保护措施,总结超大有效载荷,并告诉代理如何缩小查询范围。
---
## 开发
```bash
npm install
npm run build # 使用 tsup 打包 -> dist/index.js(可执行文件,ESM)
npm run typecheck # tsc --noEmit,严格,src + tests
npm test # vitest 运行(178 个测试)
npm run smoke # 首先构建,然后启动服务器,通过标准输入/输出进行真实测试,并调用工具
npm run dev # tsx 监视(从源代码运行服务器)
```
---
## 许可证
MIT © Morgan
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器