@ebowwa/dependency-graph-mcp 1.0.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/src/index.ts ADDED
@@ -0,0 +1,929 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Dependency Graph MCP Server
4
+ *
5
+ * Analyzes and visualizes dependency relationships in monorepos.
6
+ * Provides tools for:
7
+ * - Building dependency graphs from package.json and imports
8
+ * - Visualizing dependency relationships
9
+ * - Impact analysis for refactoring decisions
10
+ * - Finding circular dependencies
11
+ * - Identifying unused code
12
+ */
13
+
14
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
15
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16
+ import {
17
+ CallToolRequestSchema,
18
+ ListToolsRequestSchema,
19
+ } from "@modelcontextprotocol/sdk/types.js";
20
+ import { z } from "zod";
21
+ import { readdirSync, readFileSync, existsSync } from "node:fs";
22
+ import { join, dirname, relative, resolve } from "node:path";
23
+ import { fileURLToPath } from "node:url";
24
+
25
+ const TOOLING_DIR = dirname(fileURLToPath(
26
+ import.meta.url.replace("/MCP/packages/dependency-graph", "/packages/src/tooling")
27
+ ));
28
+
29
+ // ==============
30
+ // Types
31
+ // ==============
32
+
33
+ interface DependencyNode {
34
+ name: string;
35
+ path: string;
36
+ type: "package" | "workspace" | "external";
37
+ version?: string;
38
+ }
39
+
40
+ interface DependencyEdge {
41
+ from: string;
42
+ to: string;
43
+ type: "workspace" | "external" | "import";
44
+ importPath?: string;
45
+ }
46
+
47
+ interface DependencyGraph {
48
+ nodes: Map<string, DependencyNode>;
49
+ edges: DependencyEdge[];
50
+ reverseEdges: Map<string, Set<string>>;
51
+ }
52
+
53
+ interface ImportInfo {
54
+ from: string;
55
+ imports: string[];
56
+ file: string;
57
+ }
58
+
59
+ // ==============
60
+ // Graph Builder
61
+ // ==============
62
+
63
+ class DependencyGraphBuilder {
64
+ private monorepoRoot: string;
65
+ private graph: DependencyGraph;
66
+ private packageCache: Map<string, any> = new Map();
67
+
68
+ constructor(monorepoRoot: string) {
69
+ this.monorepoRoot = monorepoRoot;
70
+ this.graph = {
71
+ nodes: new Map(),
72
+ edges: [],
73
+ reverseEdges: new Map(),
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Build the complete dependency graph
79
+ */
80
+ async build(options: {
81
+ includeDevDependencies?: boolean;
82
+ analyzeImports?: boolean;
83
+ excludePatterns?: string[];
84
+ } = {}): Promise<DependencyGraph> {
85
+ const { includeDevDependencies = false, analyzeImports = true, excludePatterns = [] } = options;
86
+
87
+ // Discover all packages in the monorepo
88
+ const packages = await this.discoverPackages();
89
+
90
+ // Add nodes for all packages
91
+ for (const pkg of packages) {
92
+ this.addNode(pkg.name, pkg.path, "workspace", pkg.version);
93
+ }
94
+
95
+ // Analyze dependencies for each package
96
+ for (const pkg of packages) {
97
+ await this.analyzePackageDependencies(pkg, includeDevDependencies, excludePatterns);
98
+
99
+ if (analyzeImports) {
100
+ await this.analyzeImports(pkg);
101
+ }
102
+ }
103
+
104
+ // Build reverse edges for impact analysis
105
+ this.buildReverseEdges();
106
+
107
+ return this.graph;
108
+ }
109
+
110
+ /**
111
+ * Discover all package.json files in the monorepo
112
+ */
113
+ private async discoverPackages(): Promise<Array<{ name: string; path: string; version: string }>> {
114
+ const packages: Array<{ name: string; path: string; version: string }> = [];
115
+ const seen = new Set<string>();
116
+
117
+ // Search in common locations
118
+ const searchPaths = [
119
+ this.monorepoRoot,
120
+ join(this.monorepoRoot, "packages"),
121
+ join(this.monorepoRoot, "packages/src"),
122
+ join(this.monorepoRoot, "apps"),
123
+ join(this.monorepoRoot, "MCP/packages"),
124
+ ];
125
+
126
+ for (const searchPath of searchPaths) {
127
+ if (!existsSync(searchPath)) continue;
128
+
129
+ await this.searchDirectory(searchPath, packages, seen, 0, 4);
130
+ }
131
+
132
+ return packages;
133
+ }
134
+
135
+ /**
136
+ * Recursively search for package.json files
137
+ */
138
+ private async searchDirectory(
139
+ dir: string,
140
+ packages: Array<{ name: string; path: string; version: string }>,
141
+ seen: Set<string>,
142
+ depth: number,
143
+ maxDepth: number
144
+ ): Promise<void> {
145
+ if (depth > maxDepth) return;
146
+
147
+ try {
148
+ const entries = readdirSync(dir, { withFileTypes: true });
149
+
150
+ for (const entry of entries) {
151
+ // Skip node_modules and hidden dirs
152
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
153
+ if (entry.name === "dist" || entry.name === "build") continue;
154
+
155
+ const fullPath = join(dir, entry.name);
156
+
157
+ if (entry.isDirectory()) {
158
+ await this.searchDirectory(fullPath, packages, seen, depth + 1, maxDepth);
159
+ } else if (entry.name === "package.json") {
160
+ const packageDir = dirname(fullPath);
161
+ const relativePath = relative(this.monorepoRoot, packageDir);
162
+
163
+ if (seen.has(relativePath)) continue;
164
+ seen.add(relativePath);
165
+
166
+ try {
167
+ const pkg = JSON.parse(readFileSync(fullPath, "utf-8"));
168
+ if (pkg.name && !pkg.private) {
169
+ packages.push({
170
+ name: pkg.name,
171
+ path: relativePath,
172
+ version: pkg.version || "0.0.0",
173
+ });
174
+ this.packageCache.set(pkg.name, { path: relativePath, pkg });
175
+ }
176
+ } catch {
177
+ // Invalid package.json, skip
178
+ }
179
+ }
180
+ }
181
+ } catch {
182
+ // Directory not accessible, skip
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Analyze package.json dependencies
188
+ */
189
+ private async analyzePackageDependencies(
190
+ pkg: { name: string; path: string; version: string },
191
+ includeDev: boolean,
192
+ excludePatterns: string[]
193
+ ): Promise<void> {
194
+ const pkgPath = join(this.monorepoRoot, pkg.path, "package.json");
195
+ if (!existsSync(pkgPath)) return;
196
+
197
+ const packageJson = JSON.parse(readFileSync(pkgPath, "utf-8"));
198
+
199
+ const depTypes = ["dependencies"];
200
+ if (includeDev) depTypes.push("devDependencies", "peerDependencies", "optionalDependencies");
201
+
202
+ for (const depType of depTypes) {
203
+ const deps = packageJson[depType];
204
+ if (!deps) continue;
205
+
206
+ for (const [depName, depVersion] of Object.entries(deps as Record<string, string>)) {
207
+ // Check if this matches any exclude pattern
208
+ if (excludePatterns.some(pattern => depName.match(pattern))) continue;
209
+
210
+ const isWorkspace = depVersion === "workspace:*" || depVersion === "workspace:^" || depVersion === "workspace:~";
211
+
212
+ if (isWorkspace) {
213
+ // This is a workspace dependency
214
+ // Find the actual package in our cache
215
+ const targetPkg = this.packageCache.get(depName);
216
+ if (targetPkg) {
217
+ this.addEdge(pkg.name, depName, "workspace");
218
+ }
219
+ } else if (this.packageCache.has(depName)) {
220
+ // It's a local package but not using workspace: protocol
221
+ this.addEdge(pkg.name, depName, "workspace");
222
+ } else {
223
+ // External dependency
224
+ this.addNode(depName, "", "external", depVersion as string);
225
+ this.addEdge(pkg.name, depName, "external");
226
+ }
227
+ }
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Analyze TypeScript/JavaScript imports
233
+ */
234
+ private async analyzeImports(pkg: { name: string; path: string; version: string }): Promise<void> {
235
+ const pkgDir = join(this.monorepoRoot, pkg.path);
236
+
237
+ // Common source directories
238
+ const sourceDirs = ["src", "lib", ""];
239
+
240
+ for (const sourceDir of sourceDirs) {
241
+ const searchPath = sourceDir ? join(pkgDir, sourceDir) : pkgDir;
242
+ if (!existsSync(searchPath)) continue;
243
+
244
+ await this.analyzeImportsInDirectory(pkg.name, searchPath);
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Recursively analyze imports in a directory
250
+ */
251
+ private async analyzeImportsInDirectory(packageName: string, dir: string): Promise<void> {
252
+ try {
253
+ const entries = readdirSync(dir, { withFileTypes: true });
254
+
255
+ for (const entry of entries) {
256
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
257
+ if (entry.name === "dist" || entry.name === "build") continue;
258
+
259
+ const fullPath = join(dir, entry.name);
260
+
261
+ if (entry.isDirectory()) {
262
+ await this.analyzeImportsInDirectory(packageName, fullPath);
263
+ } else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx") || entry.name.endsWith(".js") || entry.name.endsWith(".jsx") || entry.name.endsWith(".mjs")) {
264
+ await this.analyzeImportsInFile(packageName, fullPath);
265
+ }
266
+ }
267
+ } catch {
268
+ // Directory not accessible
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Analyze imports in a single file
274
+ */
275
+ private async analyzeImportsInFile(packageName: string, filePath: string): Promise<void> {
276
+ try {
277
+ const content = readFileSync(filePath, "utf-8");
278
+
279
+ // Match various import patterns
280
+ const importPatterns = [
281
+ // ES imports: import ... from '...' | import ... from "..."
282
+ /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s*,?\s*)*\s+from\s+['"]([^'"]+)['"]/g,
283
+ // Dynamic imports: import('...')
284
+ /import\(['"]([^'"]+)['"]\)/g,
285
+ // require(): require('...') | require("...")
286
+ /require\(['"]([^'"]+)['"]\)/g,
287
+ // Export from: export ... from '...'
288
+ /export\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+)\s+from\s+)?['"]([^'"]+)['"]/g,
289
+ ];
290
+
291
+ for (const pattern of importPatterns) {
292
+ let match;
293
+ while ((match = pattern.exec(content)) !== null) {
294
+ const importPath = match[1];
295
+
296
+ // Skip relative imports
297
+ if (importPath.startsWith(".") || importPath.startsWith("/")) continue;
298
+
299
+ // Check if this is a known workspace package
300
+ for (const [pkgName, pkgData] of this.packageCache) {
301
+ if (importPath === pkgName || importPath.startsWith(pkgName + "/")) {
302
+ this.addEdge(packageName, pkgName, "import", importPath);
303
+ break;
304
+ }
305
+ }
306
+ }
307
+ }
308
+ } catch {
309
+ // File not readable
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Add a node to the graph
315
+ */
316
+ private addNode(name: string, path: string, type: "package" | "workspace" | "external", version?: string): void {
317
+ if (!this.graph.nodes.has(name)) {
318
+ this.graph.nodes.set(name, { name, path, type, version });
319
+ this.graph.reverseEdges.set(name, new Set());
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Add an edge to the graph
325
+ */
326
+ private addEdge(from: string, to: string, type: "workspace" | "external" | "import", importPath?: string): void {
327
+ // Skip self-references
328
+ if (from === to) return;
329
+
330
+ // Check if edge already exists
331
+ const existing = this.graph.edges.find(e => e.from === from && e.to === to);
332
+ if (existing) return;
333
+
334
+ this.graph.edges.push({ from, to, type, importPath });
335
+
336
+ // Update reverse edges
337
+ if (!this.graph.reverseEdges.has(to)) {
338
+ this.graph.reverseEdges.set(to, new Set());
339
+ }
340
+ this.graph.reverseEdges.get(to)!.add(from);
341
+ }
342
+
343
+ /**
344
+ * Build reverse edges for impact analysis
345
+ */
346
+ private buildReverseEdges(): void {
347
+ for (const [to, dependents] of this.graph.reverseEdges) {
348
+ // Ensure node exists
349
+ if (!this.graph.nodes.has(to)) {
350
+ this.graph.nodes.set(to, { name: to, path: "", type: "external" });
351
+ }
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Get the built graph
357
+ */
358
+ getGraph(): DependencyGraph {
359
+ return this.graph;
360
+ }
361
+ }
362
+
363
+ // ==============
364
+ // MCP Server
365
+ // ==============
366
+
367
+ const DEPENDENCY_GRAPH_SCHEMA = {
368
+ name: "dependency_graph",
369
+ description: "Build a complete dependency graph of the monorepo",
370
+ inputSchema: {
371
+ type: "object" as const,
372
+ properties: {
373
+ includeDevDependencies: {
374
+ type: "boolean" as const,
375
+ description: "Include devDependencies in the graph",
376
+ default: false,
377
+ },
378
+ analyzeImports: {
379
+ type: "boolean" as const,
380
+ description: "Analyze TypeScript/JavaScript imports",
381
+ default: true,
382
+ },
383
+ excludePatterns: {
384
+ type: "array" as const,
385
+ items: { type: "string" as const },
386
+ description: "Regex patterns to exclude from dependency analysis",
387
+ default: [],
388
+ },
389
+ format: {
390
+ type: "string" as const,
391
+ enum: ["json", "mermaid", "dot", "tree"] as const,
392
+ description: "Output format for the graph",
393
+ default: "json",
394
+ },
395
+ },
396
+ },
397
+ };
398
+
399
+ const IMPACT_ANALYSIS_SCHEMA = {
400
+ name: "impact_analysis",
401
+ description: "Analyze the impact of changing a specific package",
402
+ inputSchema: {
403
+ type: "object" as const,
404
+ properties: {
405
+ package: {
406
+ type: "string" as const,
407
+ description: "Package name to analyze",
408
+ },
409
+ includeTransitive: {
410
+ type: "boolean" as const,
411
+ description: "Include transitive dependents",
412
+ default: true,
413
+ },
414
+ format: {
415
+ type: "string" as const,
416
+ enum: ["json", "tree"] as const,
417
+ description: "Output format",
418
+ default: "tree",
419
+ },
420
+ },
421
+ required: ["package"],
422
+ },
423
+ };
424
+
425
+ const FIND_CIRCULAR_SCHEMA = {
426
+ name: "find_circular",
427
+ description: "Find circular dependencies in the monorepo",
428
+ inputSchema: {
429
+ type: "object" as const,
430
+ properties: {
431
+ maxDepth: {
432
+ type: "number" as const,
433
+ description: "Maximum depth to search for cycles",
434
+ default: 10,
435
+ },
436
+ },
437
+ },
438
+ };
439
+
440
+ const UNUSED_CODE_SCHEMA = {
441
+ name: "unused_code",
442
+ description: "Find potentially unused packages (no dependents)",
443
+ inputSchema: {
444
+ type: "object" as const,
445
+ properties: {
446
+ includeExternal: {
447
+ type: "boolean" as const,
448
+ description: "Include external dependencies",
449
+ default: false,
450
+ },
451
+ },
452
+ },
453
+ };
454
+
455
+ const PACKAGE_INFO_SCHEMA = {
456
+ name: "package_info",
457
+ description: "Get detailed information about a specific package",
458
+ inputSchema: {
459
+ type: "object" as const,
460
+ properties: {
461
+ package: {
462
+ type: "string" as const,
463
+ description: "Package name",
464
+ },
465
+ },
466
+ required: ["package"],
467
+ },
468
+ };
469
+
470
+ // Format the graph for output
471
+ function formatGraph(graph: DependencyGraph, format: string): string {
472
+ switch (format) {
473
+ case "mermaid":
474
+ return formatAsMermaid(graph);
475
+ case "dot":
476
+ return formatAsDot(graph);
477
+ case "tree":
478
+ return formatAsTree(graph);
479
+ case "json":
480
+ default:
481
+ return JSON.stringify(
482
+ {
483
+ nodes: Array.from(graph.nodes.values()),
484
+ edges: graph.edges,
485
+ },
486
+ null,
487
+ 2
488
+ );
489
+ }
490
+ }
491
+
492
+ function formatAsMermaid(graph: DependencyGraph): string {
493
+ const lines = ["graph TD"];
494
+
495
+ // Add nodes
496
+ for (const [name, node] of graph.nodes) {
497
+ const label = node.type === "workspace" ? `📦 ${name}` : `📚 ${name}`;
498
+ lines.push(` ${name.replace(/[^a-zA-Z0-9]/g, "_")}["${label}"]`);
499
+ }
500
+
501
+ // Add edges
502
+ for (const edge of graph.edges) {
503
+ const from = edge.from.replace(/[^a-zA-Z0-9]/g, "_");
504
+ const to = edge.to.replace(/[^a-zA-Z0-9]/g, "_");
505
+ const label = edge.type === "workspace" ? "workspace" : edge.type === "import" ? "imports" : "external";
506
+ lines.push(` ${from} -->|${label}| ${to}`);
507
+ }
508
+
509
+ // Add styles
510
+ lines.push(' classDef workspace fill:#e1f5fe');
511
+ lines.push(' classDef external fill:#f5f5f5');
512
+ lines.push(' classDef import fill:#fff3e0');
513
+
514
+ for (const [name, node] of graph.nodes) {
515
+ const id = name.replace(/[^a-zA-Z0-9]/g, "_");
516
+ if (node.type === "workspace") {
517
+ lines.push(` class ${id} workspace`);
518
+ } else if (node.type === "external") {
519
+ lines.push(` class ${id} external`);
520
+ }
521
+ }
522
+
523
+ return lines.join("\n");
524
+ }
525
+
526
+ function formatAsDot(graph: DependencyGraph): string {
527
+ const lines = ["digraph dependencies {"];
528
+ lines.push(' rankdir=LR;');
529
+ lines.push(' node [shape=box];');
530
+
531
+ // Add nodes
532
+ for (const [name, node] of graph.nodes) {
533
+ const color = node.type === "workspace" ? "lightblue" : "lightgray";
534
+ lines.push(` "${name}" [fillcolor=${color}, style=filled];`);
535
+ }
536
+
537
+ // Add edges
538
+ for (const edge of graph.edges) {
539
+ const style = edge.type === "workspace" ? "solid" : "dashed";
540
+ lines.push(` "${edge.from}" -> "${edge.to}" [style=${style}, label="${edge.type}"];`);
541
+ }
542
+
543
+ lines.push("}");
544
+ return lines.join("\n");
545
+ }
546
+
547
+ function formatAsTree(graph: DependencyGraph): string {
548
+ const lines: string[] = [];
549
+ const seen = new Set<string>();
550
+
551
+ // Find root nodes (no incoming edges from other workspace packages)
552
+ const workspaceNodes = Array.from(graph.nodes.values()).filter(n => n.type === "workspace");
553
+ const incomingEdges = new Map<string, number>();
554
+
555
+ for (const node of workspaceNodes) {
556
+ incomingEdges.set(node.name, 0);
557
+ }
558
+
559
+ for (const edge of graph.edges) {
560
+ if (graph.nodes.get(edge.to)?.type === "workspace") {
561
+ incomingEdges.set(edge.to, (incomingEdges.get(edge.to) || 0) + 1);
562
+ }
563
+ }
564
+
565
+ // Start from roots
566
+ for (const [name, node] of graph.nodes) {
567
+ if (node.type === "workspace" && (incomingEdges.get(name) || 0) === 0) {
568
+ lines.push(`📦 ${name}`);
569
+ printTree(graph, name, "", seen, lines);
570
+ }
571
+ }
572
+
573
+ // Add any disconnected nodes
574
+ for (const [name, node] of graph.nodes) {
575
+ if (!seen.has(name) && node.type === "workspace") {
576
+ lines.push(`📦 ${name} (disconnected)`);
577
+ }
578
+ }
579
+
580
+ return lines.join("\n");
581
+ }
582
+
583
+ function printTree(
584
+ graph: DependencyGraph,
585
+ nodeName: string,
586
+ prefix: string,
587
+ seen: Set<string>,
588
+ lines: string[]
589
+ ): void {
590
+ seen.add(nodeName);
591
+
592
+ // Find outgoing edges to other workspace packages
593
+ const outgoing = graph.edges.filter(e => e.from === nodeName && graph.nodes.get(e.to)?.type === "workspace");
594
+
595
+ for (let i = 0; i < outgoing.length; i++) {
596
+ const edge = outgoing[i];
597
+ const isLast = i === outgoing.length - 1;
598
+ const connector = isLast ? "└──" : "├──";
599
+ const childPrefix = prefix + (isLast ? " " : "│ ");
600
+
601
+ lines.push(`${prefix}${connector} 📦 ${edge.to} [${edge.type}]`);
602
+ if (!seen.has(edge.to)) {
603
+ printTree(graph, edge.to, childPrefix, seen, lines);
604
+ }
605
+ }
606
+
607
+ // Show external dependencies count
608
+ const externals = graph.edges.filter(e => e.from === nodeName && graph.nodes.get(e.to)?.type === "external");
609
+ if (externals.length > 0) {
610
+ lines.push(`${prefix}└── 📚 ${externals.length} external dependencies`);
611
+ }
612
+ }
613
+
614
+ // Find circular dependencies using DFS
615
+ function findCircularDependencies(graph: DependencyGraph, maxDepth: number = 10): string[][] {
616
+ const cycles: string[][] = [];
617
+ const visited = new Set<string>();
618
+ const recursionStack = new Set<string>();
619
+
620
+ function dfs(node: string, path: string[]): void {
621
+ if (path.length > maxDepth) return;
622
+
623
+ visited.add(node);
624
+ recursionStack.add(node);
625
+ path.push(node);
626
+
627
+ // Check all outgoing edges
628
+ for (const edge of graph.edges) {
629
+ if (edge.from === node) {
630
+ if (recursionStack.has(edge.to)) {
631
+ // Found a cycle
632
+ const cycleStart = path.indexOf(edge.to);
633
+ if (cycleStart >= 0) {
634
+ cycles.push([...path.slice(cycleStart), edge.to]);
635
+ }
636
+ } else if (!visited.has(edge.to)) {
637
+ dfs(edge.to, [...path]);
638
+ }
639
+ }
640
+ }
641
+
642
+ recursionStack.delete(node);
643
+ }
644
+
645
+ // Start DFS from each workspace node
646
+ for (const [name, node] of graph.nodes) {
647
+ if (node.type === "workspace" && !visited.has(name)) {
648
+ dfs(name, []);
649
+ }
650
+ }
651
+
652
+ return cycles;
653
+ }
654
+
655
+ // Impact analysis
656
+ function analyzeImpact(graph: DependencyGraph, packageName: string, includeTransitive: boolean = true): {
657
+ direct: string[];
658
+ transitive: string[];
659
+ all: string[];
660
+ } {
661
+ const direct: string[] = [];
662
+ const transitive: string[] = [];
663
+ const visited = new Set<string>();
664
+
665
+ // Get direct dependents
666
+ const directDependents = graph.reverseEdges.get(packageName);
667
+ if (directDependents) {
668
+ for (const dep of directDependents) {
669
+ direct.push(dep);
670
+ visited.add(dep);
671
+ }
672
+ }
673
+
674
+ // Get transitive dependents
675
+ if (includeTransitive) {
676
+ for (const dep of direct) {
677
+ collectTransitive(graph, dep, visited, transitive);
678
+ }
679
+ }
680
+
681
+ return {
682
+ direct,
683
+ transitive,
684
+ all: Array.from(visited),
685
+ };
686
+ }
687
+
688
+ function collectTransitive(graph: DependencyGraph, packageName: string, visited: Set<string>, result: string[]): void {
689
+ const dependents = graph.reverseEdges.get(packageName);
690
+ if (!dependents) return;
691
+
692
+ for (const dep of dependents) {
693
+ if (!visited.has(dep)) {
694
+ visited.add(dep);
695
+ result.push(dep);
696
+ collectTransitive(graph, dep, visited, result);
697
+ }
698
+ }
699
+ }
700
+
701
+ // Start the MCP server
702
+ const server = new Server(
703
+ {
704
+ name: "@mcp/dependency-graph",
705
+ version: "0.1.0",
706
+ },
707
+ {
708
+ capabilities: {
709
+ tools: {},
710
+ },
711
+ }
712
+ );
713
+
714
+ let cachedGraph: DependencyGraph | null = null;
715
+ let cachedBuilder: DependencyGraphBuilder | null = null;
716
+
717
+ // List tools
718
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
719
+ tools: [
720
+ DEPENDENCY_GRAPH_SCHEMA,
721
+ IMPACT_ANALYSIS_SCHEMA,
722
+ FIND_CIRCULAR_SCHEMA,
723
+ UNUSED_CODE_SCHEMA,
724
+ PACKAGE_INFO_SCHEMA,
725
+ ],
726
+ }));
727
+
728
+ // Handle tool calls
729
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
730
+ const { name, arguments: args } = request.params;
731
+
732
+ // Initialize graph builder if needed
733
+ if (!cachedBuilder) {
734
+ const monorepoRoot = process.cwd();
735
+ cachedBuilder = new DependencyGraphBuilder(monorepoRoot);
736
+ }
737
+
738
+ // Auto-build graph if not cached
739
+ if (!cachedGraph) {
740
+ await cachedBuilder.build();
741
+ cachedGraph = cachedBuilder.getGraph();
742
+ }
743
+
744
+ try {
745
+ switch (name) {
746
+ case "dependency_graph": {
747
+ const {
748
+ includeDevDependencies = false,
749
+ analyzeImports = true,
750
+ excludePatterns = [],
751
+ format = "json",
752
+ } = args as {
753
+ includeDevDependencies?: boolean;
754
+ analyzeImports?: boolean;
755
+ excludePatterns?: string[];
756
+ format?: "json" | "mermaid" | "dot" | "tree";
757
+ };
758
+
759
+ await cachedBuilder.build({ includeDevDependencies, analyzeImports, excludePatterns });
760
+ const graph = cachedBuilder.getGraph();
761
+ cachedGraph = graph;
762
+
763
+ return {
764
+ content: [{ type: "text", text: formatGraph(graph, format) }],
765
+ };
766
+ }
767
+
768
+ case "impact_analysis": {
769
+ const { package: packageName, includeTransitive = true, format = "tree" } = args as {
770
+ package: string;
771
+ includeTransitive?: boolean;
772
+ format?: "json" | "tree";
773
+ };
774
+
775
+ const impact = analyzeImpact(cachedGraph, packageName, includeTransitive);
776
+
777
+ if (format === "json") {
778
+ return {
779
+ content: [{ type: "text", text: JSON.stringify(impact, null, 2) }],
780
+ };
781
+ } else {
782
+ const lines = [`Impact analysis for: ${packageName}`];
783
+ lines.push(`\nDirect dependents (${impact.direct.length}):`);
784
+ for (const dep of impact.direct) {
785
+ lines.push(` └── ${dep}`);
786
+ }
787
+ if (includeTransitive && impact.transitive.length > 0) {
788
+ lines.push(`\nTransitive dependents (${impact.transitive.length}):`);
789
+ for (const dep of impact.transitive) {
790
+ lines.push(` └── ${dep}`);
791
+ }
792
+ }
793
+ lines.push(`\nTotal affected: ${impact.all.length} packages`);
794
+ return { content: [{ type: "text", text: lines.join("\n") }] };
795
+ }
796
+ }
797
+
798
+ case "find_circular": {
799
+ const { maxDepth = 10 } = args as { maxDepth?: number };
800
+
801
+ const cycles = findCircularDependencies(cachedGraph, maxDepth);
802
+
803
+ if (cycles.length === 0) {
804
+ return {
805
+ content: [{ type: "text", text: "No circular dependencies found!" }],
806
+ };
807
+ }
808
+
809
+ const lines = [`Found ${cycles.length} circular dependencies:\n`];
810
+ for (let i = 0; i < cycles.length; i++) {
811
+ lines.push(`Cycle ${i + 1}:`);
812
+ lines.push(` ${cycles[i].join(" → ")}`);
813
+ lines.push("");
814
+ }
815
+
816
+ return { content: [{ type: "text", text: lines.join("\n") }] };
817
+ }
818
+
819
+ case "unused_code": {
820
+ const { includeExternal = false } = args as { includeExternal?: boolean };
821
+
822
+ const unused: string[] = [];
823
+
824
+ for (const [name, node] of cachedGraph.nodes) {
825
+ if (node.type === "workspace" || includeExternal) {
826
+ const dependents = cachedGraph.reverseEdges.get(name);
827
+ if (!dependents || dependents.size === 0) {
828
+ unused.push(name);
829
+ }
830
+ }
831
+ }
832
+
833
+ if (unused.length === 0) {
834
+ return {
835
+ content: [{ type: "text", text: "No unused packages found!" }],
836
+ };
837
+ }
838
+
839
+ return {
840
+ content: [
841
+ {
842
+ type: "text",
843
+ text: `Found ${unused.length} potentially unused packages:\n\n${unused.map(n => ` └── ${n}`).join("\n")}`,
844
+ },
845
+ ],
846
+ };
847
+ }
848
+
849
+ case "package_info": {
850
+ const { package: packageName } = args as { package: string };
851
+
852
+ const node = cachedGraph.nodes.get(packageName);
853
+ if (!node) {
854
+ return {
855
+ content: [{ type: "text", text: `Package "${packageName}" not found in dependency graph.` }],
856
+ };
857
+ }
858
+
859
+ // Get dependencies
860
+ const dependencies = cachedGraph.edges.filter(e => e.from === packageName);
861
+ // Get dependents
862
+ const dependents = Array.from(cachedGraph.reverseEdges.get(packageName) || []);
863
+
864
+ const lines = [
865
+ `Package: ${packageName}`,
866
+ `Type: ${node.type}`,
867
+ `Path: ${node.path || "N/A"}`,
868
+ node.version ? `Version: ${node.version}` : "",
869
+ "",
870
+ `Dependencies (${dependencies.length}):`,
871
+ ];
872
+
873
+ for (const dep of dependencies) {
874
+ const depNode = cachedGraph.nodes.get(dep.to);
875
+ lines.push(` └── ${dep.to} [${dep.type}]${depNode?.version ? ` @ ${depNode.version}` : ""}`);
876
+ }
877
+
878
+ lines.push(``, `Dependents (${dependents.length}):`);
879
+ for (const dep of dependents) {
880
+ lines.push(` └── ${dep}`);
881
+ }
882
+
883
+ return { content: [{ type: "text", text: lines.join("\n") }] };
884
+ }
885
+
886
+ default:
887
+ throw new Error(`Unknown tool: ${name}`);
888
+ }
889
+ } catch (error) {
890
+ return {
891
+ content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
892
+ isError: true,
893
+ };
894
+ }
895
+ });
896
+
897
+ // Start server
898
+ async function main() {
899
+ // Set up error handling
900
+ server.onerror = (error) => console.error('[MCP] Error:', error);
901
+
902
+ // Signal handlers for graceful shutdown
903
+ process.on('SIGINT', async () => {
904
+ console.error('[MCP] Shutting down...');
905
+ await server.close();
906
+ process.exit(0);
907
+ });
908
+
909
+ process.on('SIGTERM', async () => {
910
+ console.error('[MCP] Shutting down...');
911
+ await server.close();
912
+ process.exit(0);
913
+ });
914
+
915
+ // Connect transport
916
+ const transport = new StdioServerTransport();
917
+ await server.connect(transport);
918
+
919
+ // Log to stderr (doesn't interfere with JSON-RPC)
920
+ console.error('[MCP] @mcp/dependency-graph server running on stdio');
921
+
922
+ // Keep stdin open for requests
923
+ process.stdin.resume();
924
+ }
925
+
926
+ main().catch((error) => {
927
+ console.error('[MCP] Fatal error:', error);
928
+ process.exit(1);
929
+ });