typegraph-mcp 0.9.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.
@@ -0,0 +1,23 @@
1
+ ---
2
+ description: Run typegraph-mcp health checks to verify setup
3
+ argument-hint: [--verbose]
4
+ ---
5
+
6
+ # TypeGraph Health Check
7
+
8
+ Run health checks to verify typegraph-mcp is correctly set up for this project.
9
+
10
+ ## Instructions
11
+
12
+ 1. Run the health check command:
13
+
14
+ ```bash
15
+ npx tsx ${CLAUDE_PLUGIN_ROOT}/cli.ts check
16
+ ```
17
+
18
+ 2. Parse the output and report results:
19
+ - Count of passed/failed/warned checks
20
+ - For any failures, highlight the issue and the suggested fix
21
+ - If all checks pass, confirm typegraph-mcp is ready
22
+
23
+ 3. The check verifies: Node.js version, tsx availability, TypeScript installation, tsconfig.json, MCP registration, dependencies, oxc-parser, oxc-resolver, tsserver, module graph, ESLint ignores, and .gitignore configuration.
@@ -0,0 +1,23 @@
1
+ ---
2
+ description: Run smoke tests to verify all 14 typegraph-mcp tools work
3
+ argument-hint: [--verbose]
4
+ ---
5
+
6
+ # TypeGraph Smoke Test
7
+
8
+ Run smoke tests that exercise all 14 tools against the current project.
9
+
10
+ ## Instructions
11
+
12
+ 1. Run the smoke test command:
13
+
14
+ ```bash
15
+ npx tsx ${CLAUDE_PLUGIN_ROOT}/cli.ts test
16
+ ```
17
+
18
+ 2. Parse the output and report results:
19
+ - Total passed/failed/skipped
20
+ - For any failures, report the tool name and what went wrong
21
+ - If all pass, confirm all 14 tools are working
22
+
23
+ 3. Tests dynamically discover a suitable file in the project and exercise: module graph build, dependency_tree, dependents, import_cycles, shortest_path, subgraph, module_boundary, navbar, find_symbol, definition, references, type_info, navigate_to, blast_radius, module_exports, and trace_chain.
package/config.ts ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Shared configuration for typegraph-mcp.
3
+ *
4
+ * Extracts the project root detection logic used by server.ts, check.ts,
5
+ * and smoke-test.ts into a single module.
6
+ */
7
+
8
+ import * as path from "node:path";
9
+
10
+ // ─── Types ───────────────────────────────────────────────────────────────────
11
+
12
+ export interface TypegraphConfig {
13
+ /** Absolute path to the target project root */
14
+ projectRoot: string;
15
+ /** Relative tsconfig path (e.g. "./tsconfig.json") */
16
+ tsconfigPath: string;
17
+ /** Absolute path to the typegraph-mcp tool directory */
18
+ toolDir: string;
19
+ /** Whether typegraph-mcp is embedded inside the project (e.g. plugins/typegraph-mcp/) */
20
+ toolIsEmbedded: boolean;
21
+ /** Path to tool dir — relative to projectRoot if embedded, else absolute */
22
+ toolRelPath: string;
23
+ }
24
+
25
+ // ─── Resolution ──────────────────────────────────────────────────────────────
26
+
27
+ /**
28
+ * Resolve typegraph-mcp configuration from the tool directory location.
29
+ *
30
+ * Project root detection (three-level fallback):
31
+ * 1. TYPEGRAPH_PROJECT_ROOT env var (explicit override)
32
+ * 2. If toolDir is inside a `plugins/` directory, go up two levels
33
+ * 3. Otherwise, use cwd (standalone deployment, run from target project)
34
+ */
35
+ export function resolveConfig(toolDir: string): TypegraphConfig {
36
+ const cwd = process.cwd();
37
+
38
+ const projectRoot = process.env["TYPEGRAPH_PROJECT_ROOT"]
39
+ ? path.resolve(cwd, process.env["TYPEGRAPH_PROJECT_ROOT"])
40
+ : path.basename(path.dirname(toolDir)) === "plugins"
41
+ ? path.resolve(toolDir, "../..")
42
+ : cwd;
43
+
44
+ const tsconfigPath = process.env["TYPEGRAPH_TSCONFIG"] || "./tsconfig.json";
45
+
46
+ const toolIsEmbedded = toolDir.startsWith(projectRoot + path.sep);
47
+ const toolRelPath = toolIsEmbedded ? path.relative(projectRoot, toolDir) : toolDir;
48
+
49
+ return { projectRoot, tsconfigPath, toolDir, toolIsEmbedded, toolRelPath };
50
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "typegraph",
3
+ "version": "1.0.0",
4
+ "description": "Type-aware TypeScript navigation — 14 MCP tools for go-to-definition, find-references, dependency graphs, cycle detection, and impact analysis",
5
+ "mcpServers": {
6
+ "typegraph": {
7
+ "command": "npx",
8
+ "args": ["tsx", "${extensionPath}/server.ts"],
9
+ "cwd": "${extensionPath}",
10
+ "env": {
11
+ "TYPEGRAPH_PROJECT_ROOT": ".",
12
+ "TYPEGRAPH_TSCONFIG": "./tsconfig.json"
13
+ }
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,462 @@
1
+ /**
2
+ * Graph Queries — Pure traversal functions for the module import graph.
3
+ *
4
+ * All functions take ModuleGraph as first parameter and are side-effect free.
5
+ * Paths in results are absolute; callers convert to relative for tool output.
6
+ */
7
+
8
+ import type { ModuleGraph, ImportEdge } from "./module-graph.js";
9
+ import * as fs from "node:fs";
10
+ import * as path from "node:path";
11
+
12
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
13
+
14
+ function shouldIncludeEdge(edge: ImportEdge, includeTypeOnly: boolean): boolean {
15
+ if (!includeTypeOnly && edge.isTypeOnly) return false;
16
+ return true;
17
+ }
18
+
19
+ // ─── 1. dependencyTree ──────────────────────────────────────────────────────
20
+
21
+ export interface DepTreeOpts {
22
+ depth?: number; // default: unlimited
23
+ includeTypeOnly?: boolean; // default: false
24
+ }
25
+
26
+ export interface DepTreeResult {
27
+ root: string;
28
+ nodes: number;
29
+ files: string[];
30
+ }
31
+
32
+ /**
33
+ * BFS forward traversal from `file`. Returns transitive dependencies
34
+ * in breadth-first order (direct deps first, then their deps, etc.).
35
+ */
36
+ export function dependencyTree(
37
+ graph: ModuleGraph,
38
+ file: string,
39
+ opts: DepTreeOpts = {}
40
+ ): DepTreeResult {
41
+ const { depth = Infinity, includeTypeOnly = false } = opts;
42
+ const visited = new Set<string>();
43
+ const result: string[] = [];
44
+
45
+ // BFS
46
+ let frontier = [file];
47
+ visited.add(file);
48
+ let currentDepth = 0;
49
+
50
+ while (frontier.length > 0 && currentDepth < depth) {
51
+ const nextFrontier: string[] = [];
52
+ for (const f of frontier) {
53
+ const edges = graph.forward.get(f) ?? [];
54
+ for (const edge of edges) {
55
+ if (!shouldIncludeEdge(edge, includeTypeOnly)) continue;
56
+ if (visited.has(edge.target)) continue;
57
+ visited.add(edge.target);
58
+ result.push(edge.target);
59
+ nextFrontier.push(edge.target);
60
+ }
61
+ }
62
+ frontier = nextFrontier;
63
+ currentDepth++;
64
+ }
65
+
66
+ return { root: file, nodes: result.length, files: result };
67
+ }
68
+
69
+ // ─── 2. dependents ──────────────────────────────────────────────────────────
70
+
71
+ export interface DependentsOpts {
72
+ depth?: number; // default: unlimited
73
+ includeTypeOnly?: boolean; // default: false
74
+ }
75
+
76
+ export interface DependentsResult {
77
+ root: string;
78
+ nodes: number;
79
+ directCount: number;
80
+ files: string[];
81
+ byPackage: Record<string, string[]>;
82
+ }
83
+
84
+ /** Cache for package.json lookups — maps directory to package name */
85
+ const packageNameCache = new Map<string, string>();
86
+
87
+ function findPackageName(filePath: string): string {
88
+ let dir = path.dirname(filePath);
89
+ while (dir !== path.dirname(dir)) {
90
+ if (packageNameCache.has(dir)) return packageNameCache.get(dir)!;
91
+ const pkgJsonPath = path.join(dir, "package.json");
92
+ try {
93
+ if (fs.existsSync(pkgJsonPath)) {
94
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
95
+ const name = pkg.name ?? path.basename(dir);
96
+ packageNameCache.set(dir, name);
97
+ return name;
98
+ }
99
+ } catch {
100
+ // Skip unreadable package.json
101
+ }
102
+ dir = path.dirname(dir);
103
+ }
104
+ return "<root>";
105
+ }
106
+
107
+ /**
108
+ * BFS reverse traversal from `file`. Returns files that (transitively) depend on this file.
109
+ * Groups results by nearest package.json ancestor.
110
+ */
111
+ export function dependents(
112
+ graph: ModuleGraph,
113
+ file: string,
114
+ opts: DependentsOpts = {}
115
+ ): DependentsResult {
116
+ const { depth = Infinity, includeTypeOnly = false } = opts;
117
+ const visited = new Set<string>();
118
+ const result: string[] = [];
119
+ let directCount = 0;
120
+
121
+ let frontier = [file];
122
+ visited.add(file);
123
+ let currentDepth = 0;
124
+
125
+ while (frontier.length > 0 && currentDepth < depth) {
126
+ const nextFrontier: string[] = [];
127
+ for (const f of frontier) {
128
+ const edges = graph.reverse.get(f) ?? [];
129
+ for (const edge of edges) {
130
+ if (!shouldIncludeEdge(edge, includeTypeOnly)) continue;
131
+ if (visited.has(edge.target)) continue;
132
+ visited.add(edge.target);
133
+ result.push(edge.target);
134
+ if (currentDepth === 0) directCount++;
135
+ nextFrontier.push(edge.target);
136
+ }
137
+ }
138
+ frontier = nextFrontier;
139
+ currentDepth++;
140
+ }
141
+
142
+ // Group by package
143
+ const byPackage: Record<string, string[]> = {};
144
+ for (const f of result) {
145
+ const pkgName = findPackageName(f);
146
+ if (!byPackage[pkgName]) byPackage[pkgName] = [];
147
+ byPackage[pkgName]!.push(f);
148
+ }
149
+
150
+ return { root: file, nodes: result.length, directCount, files: result, byPackage };
151
+ }
152
+
153
+ // ─── 3. importCycles ────────────────────────────────────────────────────────
154
+
155
+ export interface CycleOpts {
156
+ file?: string; // filter to cycles containing this file
157
+ package?: string; // filter to cycles within this directory
158
+ }
159
+
160
+ export interface CycleResult {
161
+ count: number;
162
+ cycles: string[][];
163
+ }
164
+
165
+ /**
166
+ * Find import cycles using Tarjan's SCC algorithm.
167
+ * Returns strongly connected components with more than 1 node.
168
+ */
169
+ export function importCycles(
170
+ graph: ModuleGraph,
171
+ opts: CycleOpts = {}
172
+ ): CycleResult {
173
+ const { file, package: pkgDir } = opts;
174
+
175
+ // Tarjan's SCC
176
+ let index = 0;
177
+ const stack: string[] = [];
178
+ const onStack = new Set<string>();
179
+ const indices = new Map<string, number>();
180
+ const lowlinks = new Map<string, number>();
181
+ const sccs: string[][] = [];
182
+
183
+ function strongconnect(v: string): void {
184
+ indices.set(v, index);
185
+ lowlinks.set(v, index);
186
+ index++;
187
+ stack.push(v);
188
+ onStack.add(v);
189
+
190
+ const edges = graph.forward.get(v) ?? [];
191
+ for (const edge of edges) {
192
+ const w = edge.target;
193
+ if (!graph.files.has(w)) continue; // skip external
194
+ if (!indices.has(w)) {
195
+ strongconnect(w);
196
+ lowlinks.set(v, Math.min(lowlinks.get(v)!, lowlinks.get(w)!));
197
+ } else if (onStack.has(w)) {
198
+ lowlinks.set(v, Math.min(lowlinks.get(v)!, indices.get(w)!));
199
+ }
200
+ }
201
+
202
+ // Root of SCC?
203
+ if (lowlinks.get(v) === indices.get(v)) {
204
+ const scc: string[] = [];
205
+ let w: string;
206
+ do {
207
+ w = stack.pop()!;
208
+ onStack.delete(w);
209
+ scc.push(w);
210
+ } while (w !== v);
211
+ if (scc.length > 1) {
212
+ sccs.push(scc);
213
+ }
214
+ }
215
+ }
216
+
217
+ for (const f of graph.files) {
218
+ if (!indices.has(f)) {
219
+ strongconnect(f);
220
+ }
221
+ }
222
+
223
+ // Apply filters
224
+ let cycles = sccs;
225
+ if (file) {
226
+ cycles = cycles.filter((scc) => scc.includes(file));
227
+ }
228
+ if (pkgDir) {
229
+ const absPkgDir = path.resolve(pkgDir);
230
+ cycles = cycles.filter((scc) =>
231
+ scc.every((f) => f.startsWith(absPkgDir))
232
+ );
233
+ }
234
+
235
+ return { count: cycles.length, cycles };
236
+ }
237
+
238
+ // ─── 4. shortestPath ────────────────────────────────────────────────────────
239
+
240
+ export interface PathOpts {
241
+ includeTypeOnly?: boolean; // default: false
242
+ }
243
+
244
+ export interface PathResult {
245
+ path: string[] | null;
246
+ hops: number;
247
+ chain: Array<{ file: string; imports: string[] }>;
248
+ }
249
+
250
+ /**
251
+ * BFS on forward graph from `from` to `to`.
252
+ * Returns the shortest import path with specifier names at each hop.
253
+ */
254
+ export function shortestPath(
255
+ graph: ModuleGraph,
256
+ from: string,
257
+ to: string,
258
+ opts: PathOpts = {}
259
+ ): PathResult {
260
+ const { includeTypeOnly = false } = opts;
261
+
262
+ if (from === to) {
263
+ return { path: [from], hops: 0, chain: [{ file: from, imports: [] }] };
264
+ }
265
+
266
+ // BFS with parent tracking
267
+ const visited = new Set<string>();
268
+ const parent = new Map<string, { from: string; specifiers: string[] }>();
269
+ visited.add(from);
270
+ let frontier = [from];
271
+
272
+ while (frontier.length > 0) {
273
+ const nextFrontier: string[] = [];
274
+ for (const f of frontier) {
275
+ const edges = graph.forward.get(f) ?? [];
276
+ for (const edge of edges) {
277
+ if (!shouldIncludeEdge(edge, includeTypeOnly)) continue;
278
+ if (visited.has(edge.target)) continue;
279
+ visited.add(edge.target);
280
+ parent.set(edge.target, { from: f, specifiers: edge.specifiers });
281
+
282
+ if (edge.target === to) {
283
+ // Reconstruct path
284
+ const filePath: string[] = [to];
285
+ let current = to;
286
+ while (parent.has(current)) {
287
+ current = parent.get(current)!.from;
288
+ filePath.unshift(current);
289
+ }
290
+
291
+ const chain: Array<{ file: string; imports: string[] }> = [];
292
+ for (let i = 0; i < filePath.length; i++) {
293
+ const p = parent.get(filePath[i]!);
294
+ chain.push({
295
+ file: filePath[i]!,
296
+ imports: p?.specifiers ?? [],
297
+ });
298
+ }
299
+
300
+ return { path: filePath, hops: filePath.length - 1, chain };
301
+ }
302
+
303
+ nextFrontier.push(edge.target);
304
+ }
305
+ }
306
+ frontier = nextFrontier;
307
+ }
308
+
309
+ return { path: null, hops: -1, chain: [] };
310
+ }
311
+
312
+ // ─── 5. subgraph ────────────────────────────────────────────────────────────
313
+
314
+ export interface SubgraphOpts {
315
+ depth?: number; // default: 1
316
+ direction?: "imports" | "dependents" | "both"; // default: "both"
317
+ }
318
+
319
+ export interface SubgraphResult {
320
+ nodes: string[];
321
+ edges: Array<{
322
+ from: string;
323
+ to: string;
324
+ specifiers: string[];
325
+ isTypeOnly: boolean;
326
+ }>;
327
+ stats: { nodeCount: number; edgeCount: number };
328
+ }
329
+
330
+ /**
331
+ * Expand from seed files by `depth` hops in the specified direction.
332
+ * Returns the induced subgraph (all edges between discovered nodes).
333
+ */
334
+ export function subgraph(
335
+ graph: ModuleGraph,
336
+ files: string[],
337
+ opts: SubgraphOpts = {}
338
+ ): SubgraphResult {
339
+ const { depth = 1, direction = "both" } = opts;
340
+ const visited = new Set<string>(files);
341
+
342
+ let frontier = [...files];
343
+ for (let d = 0; d < depth && frontier.length > 0; d++) {
344
+ const nextFrontier: string[] = [];
345
+ for (const f of frontier) {
346
+ if (direction === "imports" || direction === "both") {
347
+ for (const edge of graph.forward.get(f) ?? []) {
348
+ if (!visited.has(edge.target)) {
349
+ visited.add(edge.target);
350
+ nextFrontier.push(edge.target);
351
+ }
352
+ }
353
+ }
354
+ if (direction === "dependents" || direction === "both") {
355
+ for (const edge of graph.reverse.get(f) ?? []) {
356
+ if (!visited.has(edge.target)) {
357
+ visited.add(edge.target);
358
+ nextFrontier.push(edge.target);
359
+ }
360
+ }
361
+ }
362
+ }
363
+ frontier = nextFrontier;
364
+ }
365
+
366
+ // Collect all edges between discovered nodes
367
+ const nodes = [...visited];
368
+ const edges: SubgraphResult["edges"] = [];
369
+ for (const f of nodes) {
370
+ for (const edge of graph.forward.get(f) ?? []) {
371
+ if (visited.has(edge.target)) {
372
+ edges.push({
373
+ from: f,
374
+ to: edge.target,
375
+ specifiers: edge.specifiers,
376
+ isTypeOnly: edge.isTypeOnly,
377
+ });
378
+ }
379
+ }
380
+ }
381
+
382
+ return { nodes, edges, stats: { nodeCount: nodes.length, edgeCount: edges.length } };
383
+ }
384
+
385
+ // ─── 6. moduleBoundary ──────────────────────────────────────────────────────
386
+
387
+ export interface BoundaryResult {
388
+ internalEdges: number;
389
+ incomingEdges: Array<{ from: string; to: string; specifiers: string[] }>;
390
+ outgoingEdges: Array<{ from: string; to: string; specifiers: string[] }>;
391
+ sharedDependencies: string[];
392
+ isolationScore: number; // 0–1
393
+ }
394
+
395
+ /**
396
+ * Cut-line analysis for a set of files.
397
+ * Identifies incoming/outgoing edges and computes an isolation score.
398
+ */
399
+ export function moduleBoundary(
400
+ graph: ModuleGraph,
401
+ files: string[]
402
+ ): BoundaryResult {
403
+ const fileSet = new Set(files);
404
+
405
+ let internalEdges = 0;
406
+ const incomingEdges: BoundaryResult["incomingEdges"] = [];
407
+ const outgoingEdges: BoundaryResult["outgoingEdges"] = [];
408
+ const outgoingTargets = new Set<string>();
409
+
410
+ // Count internal edges and outgoing edges
411
+ for (const f of files) {
412
+ for (const edge of graph.forward.get(f) ?? []) {
413
+ if (fileSet.has(edge.target)) {
414
+ internalEdges++;
415
+ } else {
416
+ outgoingEdges.push({
417
+ from: f,
418
+ to: edge.target,
419
+ specifiers: edge.specifiers,
420
+ });
421
+ outgoingTargets.add(edge.target);
422
+ }
423
+ }
424
+ }
425
+
426
+ // Find incoming edges (files outside the set that import files in the set)
427
+ for (const f of files) {
428
+ for (const edge of graph.reverse.get(f) ?? []) {
429
+ if (!fileSet.has(edge.target)) {
430
+ incomingEdges.push({
431
+ from: edge.target,
432
+ to: f,
433
+ specifiers: edge.specifiers,
434
+ });
435
+ }
436
+ }
437
+ }
438
+
439
+ // Shared dependencies: outgoing targets that are imported by multiple files in the set
440
+ const depCounts = new Map<string, number>();
441
+ for (const f of files) {
442
+ for (const edge of graph.forward.get(f) ?? []) {
443
+ if (!fileSet.has(edge.target)) {
444
+ depCounts.set(edge.target, (depCounts.get(edge.target) ?? 0) + 1);
445
+ }
446
+ }
447
+ }
448
+ const sharedDependencies = [...depCounts.entries()]
449
+ .filter(([, count]) => count > 1)
450
+ .map(([dep]) => dep);
451
+
452
+ const total = internalEdges + incomingEdges.length + outgoingEdges.length;
453
+ const isolationScore = total === 0 ? 1 : internalEdges / total;
454
+
455
+ return {
456
+ internalEdges,
457
+ incomingEdges,
458
+ outgoingEdges,
459
+ sharedDependencies,
460
+ isolationScore,
461
+ };
462
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "${CLAUDE_PLUGIN_ROOT}/scripts/ensure-deps.sh",
9
+ "timeout": 30
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }