ast-lens-mcp 0.1.0
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/LICENSE +21 -0
- package/README.md +330 -0
- package/dist/index.js +1861 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Morgan
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# ast-lens-mcp
|
|
2
|
+
|
|
3
|
+
> An MCP server that gives AI agents **structural understanding** of a TypeScript / JavaScript codebase — so an agent can *query* code structure instead of reading whole files into its context window.
|
|
4
|
+
|
|
5
|
+
`ast-lens-mcp` parses your project with the Babel AST toolchain and exposes six focused, read-only tools over the [Model Context Protocol](https://modelcontextprotocol.io). Point it at a project root and an LLM client (Claude Desktop, Cursor, or anything that speaks MCP) can ask precise structural questions: *what symbols are exported here? where is this function called? which functions are too complex? what does this module import?* — all without paging entire files through the model.
|
|
6
|
+
|
|
7
|
+
It runs entirely on your **local files**. No API keys, no network calls, no credentials.
|
|
8
|
+
|
|
9
|
+
- **Built with:** TypeScript (strict), the official [`@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` for AST analysis, [`zod`](https://zod.dev) for tool input schemas, and [`vitest`](https://vitest.dev) for tests.
|
|
10
|
+
- **Transport:** stdio. **Runtime:** Node 18+. **Module system:** ESM.
|
|
11
|
+
- **Languages analyzed:** `.ts`, `.tsx`, `.js`, `.jsx`, `.mts`, `.cts`, `.mjs`, `.cjs`. `node_modules` and build output are ignored automatically.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Why this exists
|
|
16
|
+
|
|
17
|
+
Coding agents waste a lot of context re-reading files just to answer structural questions ("does this symbol exist?", "where is it used?", "what's the shape of this class?"). Those questions have *exact* answers that come from the AST, not from an approximate read. `ast-lens-mcp` turns them into cheap, deterministic tool calls that return small structured JSON — leaving more of the model's context for actual reasoning.
|
|
18
|
+
|
|
19
|
+
It is deliberately **syntactic, not type-aware**: it parses, it does not type-check. That keeps it fast, dependency-light, and able to analyze a file in isolation (no `tsconfig` resolution, no whole-program build). `find_references` is therefore a precise *name-based* search classified by syntactic role, not a type-resolved rename index — see the note on that tool below.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# from npm (once published)
|
|
27
|
+
npm install -g ast-lens-mcp
|
|
28
|
+
|
|
29
|
+
# or run without installing
|
|
30
|
+
npx ast-lens-mcp /path/to/your/project
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
From source:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
git clone <your-repo-url> ast-lens-mcp
|
|
37
|
+
cd ast-lens-mcp
|
|
38
|
+
npm install
|
|
39
|
+
npm run build
|
|
40
|
+
node dist/index.js /path/to/your/project # boots on stdio
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The project root that tools analyze is resolved in this order:
|
|
44
|
+
|
|
45
|
+
1. a positional CLI arg or `--root <path>` flag,
|
|
46
|
+
2. the `AST_LENS_PROJECT_ROOT` environment variable,
|
|
47
|
+
3. the current working directory.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## MCP client configuration
|
|
52
|
+
|
|
53
|
+
### Claude Desktop
|
|
54
|
+
|
|
55
|
+
Add an entry to `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"mcpServers": {
|
|
60
|
+
"ast-lens": {
|
|
61
|
+
"command": "npx",
|
|
62
|
+
"args": ["-y", "ast-lens-mcp", "/absolute/path/to/your/project"]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Or, if installed globally / from source:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"mcpServers": {
|
|
73
|
+
"ast-lens": {
|
|
74
|
+
"command": "node",
|
|
75
|
+
"args": ["/absolute/path/to/ast-lens-mcp/dist/index.js"],
|
|
76
|
+
"env": {
|
|
77
|
+
"AST_LENS_PROJECT_ROOT": "/absolute/path/to/your/project"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Generic `mcp.json` (Cursor, VS Code MCP, etc.)
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"mcpServers": {
|
|
89
|
+
"ast-lens": {
|
|
90
|
+
"command": "npx",
|
|
91
|
+
"args": ["-y", "ast-lens-mcp", "${workspaceFolder}"]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
All paths passed to tools are interpreted **relative to the project root** (or absolute, as long as they stay inside it). Directory traversal outside the root is refused, and so are symlinks that resolve to a location outside the root — the server verifies the real (symlink-resolved) path before reading any file.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Tools
|
|
102
|
+
|
|
103
|
+
Every tool is read-only (`readOnlyHint: true`, `openWorldHint: false`), validates input with a strict zod schema, returns both human-readable text and machine-readable `structuredContent`, supports `response_format: "json" | "markdown"`, and never throws on bad input — parse failures come back as structured `parseErrors`.
|
|
104
|
+
|
|
105
|
+
### 1. `list_symbols`
|
|
106
|
+
|
|
107
|
+
All top-level / exported symbols (function, class, interface, type, enum, const/let/var) across a file, directory, or glob.
|
|
108
|
+
|
|
109
|
+
```jsonc
|
|
110
|
+
// input
|
|
111
|
+
{ "target": "src/models.ts", "kinds": ["function", "class"] }
|
|
112
|
+
|
|
113
|
+
// output (abridged)
|
|
114
|
+
{
|
|
115
|
+
"totalSymbols": 3,
|
|
116
|
+
"fileCount": 1,
|
|
117
|
+
"files": [
|
|
118
|
+
{
|
|
119
|
+
"file": "src/models.ts",
|
|
120
|
+
"symbols": [
|
|
121
|
+
{ "name": "nextId", "kind": "function", "exported": true,
|
|
122
|
+
"exportKind": "named", "span": { "start": {"line":21,"column":8}, "end": {"line":24,"column":2} } },
|
|
123
|
+
{ "name": "UserService", "kind": "class", "exported": true, "exportKind": "named",
|
|
124
|
+
"span": { "start": {"line":26,"column":8}, "end": {"line":44,"column":2} } }
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
"parseErrors": []
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Options: `kinds` (filter), `exportedOnly`, `ignore` (extra globs), `response_format`.
|
|
133
|
+
|
|
134
|
+
### 2. `get_file_outline`
|
|
135
|
+
|
|
136
|
+
A hierarchical outline of **one** file: classes with their methods/properties, interfaces with their members, enums with their members.
|
|
137
|
+
|
|
138
|
+
```jsonc
|
|
139
|
+
// input
|
|
140
|
+
{ "file": "src/models.ts" }
|
|
141
|
+
|
|
142
|
+
// output (abridged)
|
|
143
|
+
{
|
|
144
|
+
"file": "src/models.ts",
|
|
145
|
+
"symbolCount": 8,
|
|
146
|
+
"outline": [
|
|
147
|
+
{ "name": "User", "kind": "interface", "exported": true,
|
|
148
|
+
"children": [
|
|
149
|
+
{ "name": "id", "kind": "property", "span": { "start": {"line":4,"column":3}, ... } },
|
|
150
|
+
{ "name": "getDisplayName", "kind": "method", "span": { ... } }
|
|
151
|
+
] },
|
|
152
|
+
{ "name": "UserService", "kind": "class", "exported": true,
|
|
153
|
+
"children": [
|
|
154
|
+
{ "name": "create", "kind": "method", "static": true, "span": { ... } },
|
|
155
|
+
{ "name": "count", "kind": "getter", "span": { ... } },
|
|
156
|
+
{ "name": "findById", "kind": "method", "async": true, "span": { ... } }
|
|
157
|
+
] }
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### 3. `find_references`
|
|
163
|
+
|
|
164
|
+
Every reference to an identifier name across the project, with locations, a source snippet, and a syntactic-context classification (`call` / `import` / `declaration` / `type` / `jsx` / `reference`).
|
|
165
|
+
|
|
166
|
+
```jsonc
|
|
167
|
+
// input
|
|
168
|
+
{ "name": "UserService", "target": "src" }
|
|
169
|
+
|
|
170
|
+
// output (abridged)
|
|
171
|
+
{
|
|
172
|
+
"name": "UserService",
|
|
173
|
+
"total": 6,
|
|
174
|
+
"count": 6,
|
|
175
|
+
"references": [
|
|
176
|
+
{ "file": "src/models.ts", "context": "declaration",
|
|
177
|
+
"snippet": "export class UserService {", "span": { "start": {"line":26,"column":14}, ... } },
|
|
178
|
+
{ "file": "src/models.ts", "context": "call",
|
|
179
|
+
"snippet": "return new UserService();", "span": { ... } },
|
|
180
|
+
{ "file": "src/widget.tsx", "context": "import",
|
|
181
|
+
"snippet": "import { UserService } from \"./models\";", "span": { ... } }
|
|
182
|
+
],
|
|
183
|
+
"byContext": { "declaration": 1, "type": 1, "call": 1, "reference": 2, "import": 1 },
|
|
184
|
+
"parseErrors": [ { "file": "src/broken.ts", "message": "Unexpected keyword 'const'. (3:2)", "position": {"line":3,"column":3} } ]
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
> **Note:** this is a fast, name-based search (no full type resolution), so identical names from different scopes/modules are all reported. Use the `snippet` and `context` to disambiguate, and the `byContext` histogram to, e.g., count only call sites. Options: `target` (defaults to the whole project), `includeDeclarations`, `ignore`, `limit`, `response_format`.
|
|
189
|
+
|
|
190
|
+
### 4. `search_ast`
|
|
191
|
+
|
|
192
|
+
Structural queries over the AST — far more precise than text search. Ships a curated set of named queries plus a generic node-type escape hatch:
|
|
193
|
+
|
|
194
|
+
| query | finds |
|
|
195
|
+
|---|---|
|
|
196
|
+
| `calls_to` | calls to a specific callee (set `callee`, e.g. `"console.log"` or `"fetch"`) |
|
|
197
|
+
| `console_usage` | any `console.*` call |
|
|
198
|
+
| `any_usage` | TypeScript `any` type annotations |
|
|
199
|
+
| `non_null_assertion` | TypeScript `!` non-null assertions |
|
|
200
|
+
| `await_in_loop` | `await` inside `for`/`while` loops (a sequential-await perf smell) |
|
|
201
|
+
| `empty_catch` | `catch` clauses with an empty body (swallowed errors) |
|
|
202
|
+
| `todo_fixme` | `TODO` / `FIXME` / `HACK` / `XXX` markers in comments |
|
|
203
|
+
| `ts_ignore` | `@ts-ignore` / `@ts-expect-error` / `@ts-nocheck` suppression comments |
|
|
204
|
+
| `node_type` | generic: match any Babel node type (set `nodeType`, e.g. `"TryStatement"`) |
|
|
205
|
+
|
|
206
|
+
```jsonc
|
|
207
|
+
// input
|
|
208
|
+
{ "query": "await_in_loop", "target": "src/smelly.ts" }
|
|
209
|
+
|
|
210
|
+
// output
|
|
211
|
+
{
|
|
212
|
+
"query": "await_in_loop",
|
|
213
|
+
"total": 1,
|
|
214
|
+
"matches": [
|
|
215
|
+
{ "file": "src/smelly.ts", "nodeType": "AwaitExpression",
|
|
216
|
+
"snippet": "const result = await fetchValue(item); // await in loop",
|
|
217
|
+
"span": { "start": {"line":8,"column":20}, "end": {"line":8,"column":42} } }
|
|
218
|
+
],
|
|
219
|
+
"parseErrors": []
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### 5. `analyze_complexity`
|
|
224
|
+
|
|
225
|
+
Per-function cyclomatic complexity (McCabe: `1 + decision points` — `if`, loops, each non-default `case`, `catch`, ternary, `&&`/`||`/`??`, and optional chaining) plus lines-of-code, with a threshold flag. Nested functions are measured independently.
|
|
226
|
+
|
|
227
|
+
```jsonc
|
|
228
|
+
// input
|
|
229
|
+
{ "target": "src/smelly.ts", "threshold": 5, "flaggedOnly": true }
|
|
230
|
+
|
|
231
|
+
// output
|
|
232
|
+
{
|
|
233
|
+
"threshold": 5,
|
|
234
|
+
"totalFunctions": 4,
|
|
235
|
+
"flaggedCount": 1,
|
|
236
|
+
"maxComplexity": 6,
|
|
237
|
+
"averageComplexity": 2.75,
|
|
238
|
+
"files": [
|
|
239
|
+
{ "file": "src/smelly.ts",
|
|
240
|
+
"functions": [
|
|
241
|
+
{ "name": "classify", "kind": "function", "complexity": 6, "loc": 16, "params": 1,
|
|
242
|
+
"overThreshold": true, "span": { "start": {"line":23,"column":8}, ... } }
|
|
243
|
+
] }
|
|
244
|
+
]
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Options: `threshold`, `flaggedOnly`, `sortBy` (`complexity` | `loc` | `location`), `ignore`, `limit`, `response_format`.
|
|
249
|
+
|
|
250
|
+
### 6. `summarize_module`
|
|
251
|
+
|
|
252
|
+
One file's imports, exports, and dependencies — including static imports, re-exports, dynamic `import()` calls, and `require()` calls. Dependencies are split into local (relative/absolute paths) vs external (bare package specifiers).
|
|
253
|
+
|
|
254
|
+
```jsonc
|
|
255
|
+
// input
|
|
256
|
+
{ "file": "src/barrel.ts" }
|
|
257
|
+
|
|
258
|
+
// output (abridged)
|
|
259
|
+
{
|
|
260
|
+
"file": "src/barrel.ts",
|
|
261
|
+
"imports": [
|
|
262
|
+
{ "source": "node:fs/promises", "typeOnly": false, "bindings": [ { "local": "readFile", "imported": "readFile" } ] },
|
|
263
|
+
{ "source": "./models", "typeOnly": true, "bindings": [ { "local": "User", "imported": "User" } ] },
|
|
264
|
+
{ "source": "./models", "typeOnly": false, "bindings": [ { "local": "models", "imported": "*" } ] },
|
|
265
|
+
{ "source": "./models", "typeOnly": false, "bindings": [ { "local": "defaultService", "imported": "default" } ] }
|
|
266
|
+
],
|
|
267
|
+
"exports": [
|
|
268
|
+
{ "name": "Role", "kind": "named", "source": "./models", "typeOnly": false },
|
|
269
|
+
{ "name": "*", "kind": "named", "source": "./widget", "typeOnly": false },
|
|
270
|
+
{ "name": "loadConfig", "kind": "named", "typeOnly": false }
|
|
271
|
+
],
|
|
272
|
+
"dependencies": ["./dynamic-mod", "./legacy-cjs", "./models", "./widget", "node:fs/promises"],
|
|
273
|
+
"localDependencies": ["./dynamic-mod", "./legacy-cjs", "./models", "./widget"],
|
|
274
|
+
"externalDependencies": ["node:fs/promises"],
|
|
275
|
+
"counts": { "imports": 4, "exports": 6, "dependencies": 5 }
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Architecture
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
src/
|
|
285
|
+
index.ts # bin entry — resolves project root, starts stdio transport
|
|
286
|
+
server.ts # builds the McpServer and registers all tools (transport-agnostic)
|
|
287
|
+
core/ # the reusable AST layer
|
|
288
|
+
parser.ts # Babel parse + mtime-based parse cache (never throws)
|
|
289
|
+
files.ts # glob/dir/file discovery, node_modules ignore, path-traversal sandbox
|
|
290
|
+
traverse.ts # shared traversal helpers + cyclomatic-complexity engine
|
|
291
|
+
extract.ts # symbol / outline / module-summary extraction
|
|
292
|
+
context.ts # ServerContext: shared root + cache + batch loader
|
|
293
|
+
response.ts # JSON/Markdown formatting, character-limit guard, error results
|
|
294
|
+
types.ts # shared structured-output types
|
|
295
|
+
tools/ # one file per tool, each exporting register<Tool>(server, ctx)
|
|
296
|
+
listSymbols.ts getFileOutline.ts findReferences.ts
|
|
297
|
+
searchAst.ts analyzeComplexity.ts summarizeModule.ts
|
|
298
|
+
shared.ts # shared zod schema fragments
|
|
299
|
+
test/
|
|
300
|
+
core.test.ts # parser, cache, path-safety, complexity
|
|
301
|
+
tools.test.ts # every tool, end-to-end through an in-memory MCP client
|
|
302
|
+
fixtures/sample-project/ # small, realistic fixtures (incl. a deliberately broken file)
|
|
303
|
+
scripts/
|
|
304
|
+
smoke.mjs # boots the built server over real stdio and exercises the tools
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Design notes:
|
|
308
|
+
|
|
309
|
+
- **Robust by construction.** A bad file never crashes a call: the parser returns a structured result, and batch tools collect failures into `parseErrors`. Tools that expect a single file return actionable errors when handed a directory or glob.
|
|
310
|
+
- **Sandboxed.** Tool inputs are confined to the project root; any path that resolves outside it — including via a symlink that points out of the tree — is rejected (`PathEscapeError`) before any file is read.
|
|
311
|
+
- **Context-friendly.** Responses carry a 25k-character guard that summarizes oversized payloads and tells the agent how to narrow the query.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Development
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
npm install
|
|
319
|
+
npm run build # bundle with tsup -> dist/index.js (executable, ESM)
|
|
320
|
+
npm run typecheck # tsc --noEmit, strict, over src + tests
|
|
321
|
+
npm test # vitest run (47 tests)
|
|
322
|
+
npm run smoke # build first, then boot the server over stdio and call tools
|
|
323
|
+
npm run dev # tsx watch (run the server from source)
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## License
|
|
329
|
+
|
|
330
|
+
MIT © Morgan
|