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 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