@ebowwa/dependency-graph-mcp 1.0.1 → 1.1.1

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/bun.lock CHANGED
@@ -5,6 +5,7 @@
5
5
  "": {
6
6
  "name": "@ebowwa/dependency-graph-mcp",
7
7
  "dependencies": {
8
+ "@ebowwa/dependency-graph": "^1.0.2",
8
9
  "@modelcontextprotocol/sdk": "^1.0.4",
9
10
  "zod": "^3.24.1",
10
11
  },
@@ -19,6 +20,8 @@
19
20
  },
20
21
  },
21
22
  "packages": {
23
+ "@ebowwa/dependency-graph": ["@ebowwa/dependency-graph@1.0.2", "", { "bin": { "dependency-graph": "dist/cli.js" } }, "sha512-r9wggNgQ3t+oFg/1hd4wTs29uJ8ckVa9l1mHWQXBHnrHsgvPI+fhwJOHJLbGrU0vsOpv5q8H3ikThiQDMtPFxA=="],
24
+
22
25
  "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
23
26
 
24
27
  "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.26.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg=="],
package/dist/index.js CHANGED
@@ -13267,11 +13267,9 @@ var ServerResultSchema2 = union([
13267
13267
  CreateTaskResultSchema2
13268
13268
  ]);
13269
13269
 
13270
- // src/index.ts
13271
- import { readdirSync, readFileSync, existsSync } from "fs";
13272
- import { join, dirname, relative } from "path";
13273
- import { fileURLToPath } from "url";
13274
- var TOOLING_DIR = dirname(fileURLToPath(import.meta.url.replace("/MCP/packages/dependency-graph", "/packages/src/tooling")));
13270
+ // ../../src/dependency-graph/dist/builder.js
13271
+ import { readdirSync, readFileSync, existsSync } from "node:fs";
13272
+ import { join, dirname, relative } from "node:path";
13275
13273
 
13276
13274
  class DependencyGraphBuilder {
13277
13275
  monorepoRoot;
@@ -13461,105 +13459,111 @@ class DependencyGraphBuilder {
13461
13459
  getGraph() {
13462
13460
  return this.graph;
13463
13461
  }
13462
+ getMonorepoRoot() {
13463
+ return this.monorepoRoot;
13464
+ }
13464
13465
  }
13465
- var DEPENDENCY_GRAPH_SCHEMA = {
13466
- name: "dependency_graph",
13467
- description: "Build a complete dependency graph of the monorepo",
13468
- inputSchema: {
13469
- type: "object",
13470
- properties: {
13471
- includeDevDependencies: {
13472
- type: "boolean",
13473
- description: "Include devDependencies in the graph",
13474
- default: false
13475
- },
13476
- analyzeImports: {
13477
- type: "boolean",
13478
- description: "Analyze TypeScript/JavaScript imports",
13479
- default: true
13480
- },
13481
- excludePatterns: {
13482
- type: "array",
13483
- items: { type: "string" },
13484
- description: "Regex patterns to exclude from dependency analysis",
13485
- default: []
13486
- },
13487
- format: {
13488
- type: "string",
13489
- enum: ["json", "mermaid", "dot", "tree"],
13490
- description: "Output format for the graph",
13491
- default: "json"
13466
+ // ../../src/dependency-graph/dist/analysis.js
13467
+ function findCircularDependencies(graph, maxDepth = 10) {
13468
+ const cycles = [];
13469
+ const visited = new Set;
13470
+ const recursionStack = new Set;
13471
+ function dfs(node, path) {
13472
+ if (path.length > maxDepth)
13473
+ return;
13474
+ visited.add(node);
13475
+ recursionStack.add(node);
13476
+ path.push(node);
13477
+ for (const edge of graph.edges) {
13478
+ if (edge.from === node) {
13479
+ if (recursionStack.has(edge.to)) {
13480
+ const cycleStart = path.indexOf(edge.to);
13481
+ if (cycleStart >= 0) {
13482
+ cycles.push([...path.slice(cycleStart), edge.to]);
13483
+ }
13484
+ } else if (!visited.has(edge.to)) {
13485
+ dfs(edge.to, [...path]);
13486
+ }
13492
13487
  }
13493
13488
  }
13489
+ recursionStack.delete(node);
13494
13490
  }
13495
- };
13496
- var IMPACT_ANALYSIS_SCHEMA = {
13497
- name: "impact_analysis",
13498
- description: "Analyze the impact of changing a specific package",
13499
- inputSchema: {
13500
- type: "object",
13501
- properties: {
13502
- package: {
13503
- type: "string",
13504
- description: "Package name to analyze"
13505
- },
13506
- includeTransitive: {
13507
- type: "boolean",
13508
- description: "Include transitive dependents",
13509
- default: true
13510
- },
13511
- format: {
13512
- type: "string",
13513
- enum: ["json", "tree"],
13514
- description: "Output format",
13515
- default: "tree"
13516
- }
13517
- },
13518
- required: ["package"]
13491
+ for (const [name, node] of graph.nodes) {
13492
+ if (node.type === "workspace" && !visited.has(name)) {
13493
+ dfs(name, []);
13494
+ }
13519
13495
  }
13520
- };
13521
- var FIND_CIRCULAR_SCHEMA = {
13522
- name: "find_circular",
13523
- description: "Find circular dependencies in the monorepo",
13524
- inputSchema: {
13525
- type: "object",
13526
- properties: {
13527
- maxDepth: {
13528
- type: "number",
13529
- description: "Maximum depth to search for cycles",
13530
- default: 10
13531
- }
13496
+ return cycles;
13497
+ }
13498
+ function analyzeImpact(graph, packageName, includeTransitive = true) {
13499
+ const direct = [];
13500
+ const transitive = [];
13501
+ const visited = new Set;
13502
+ const directDependents = graph.reverseEdges.get(packageName);
13503
+ if (directDependents) {
13504
+ for (const dep of directDependents) {
13505
+ direct.push(dep);
13506
+ visited.add(dep);
13532
13507
  }
13533
13508
  }
13534
- };
13535
- var UNUSED_CODE_SCHEMA = {
13536
- name: "unused_code",
13537
- description: "Find potentially unused packages (no dependents)",
13538
- inputSchema: {
13539
- type: "object",
13540
- properties: {
13541
- includeExternal: {
13542
- type: "boolean",
13543
- description: "Include external dependencies",
13544
- default: false
13545
- }
13509
+ if (includeTransitive) {
13510
+ for (const dep of direct) {
13511
+ collectTransitive(graph, dep, visited, transitive);
13546
13512
  }
13547
13513
  }
13548
- };
13549
- var PACKAGE_INFO_SCHEMA = {
13550
- name: "package_info",
13551
- description: "Get detailed information about a specific package",
13552
- inputSchema: {
13553
- type: "object",
13554
- properties: {
13555
- package: {
13556
- type: "string",
13557
- description: "Package name"
13514
+ return {
13515
+ direct,
13516
+ transitive,
13517
+ all: Array.from(visited)
13518
+ };
13519
+ }
13520
+ function collectTransitive(graph, packageName, visited, result) {
13521
+ const dependents = graph.reverseEdges.get(packageName);
13522
+ if (!dependents)
13523
+ return;
13524
+ for (const dep of dependents) {
13525
+ if (!visited.has(dep)) {
13526
+ visited.add(dep);
13527
+ result.push(dep);
13528
+ collectTransitive(graph, dep, visited, result);
13529
+ }
13530
+ }
13531
+ }
13532
+ function findUnusedPackages(graph, includeExternal = false) {
13533
+ const unused = [];
13534
+ for (const [name, node] of graph.nodes) {
13535
+ if (node.type === "workspace" || includeExternal) {
13536
+ const dependents = graph.reverseEdges.get(name);
13537
+ if (!dependents || dependents.size === 0) {
13538
+ unused.push(name);
13558
13539
  }
13559
- },
13560
- required: ["package"]
13540
+ }
13561
13541
  }
13562
- };
13542
+ return unused;
13543
+ }
13544
+ function getPackageInfo(graph, packageName) {
13545
+ const node = graph.nodes.get(packageName);
13546
+ if (!node)
13547
+ return null;
13548
+ const dependencies = graph.edges.filter((e) => e.from === packageName).map((edge) => {
13549
+ const depNode = graph.nodes.get(edge.to);
13550
+ return {
13551
+ name: edge.to,
13552
+ type: edge.type,
13553
+ version: depNode?.version
13554
+ };
13555
+ });
13556
+ const dependents = Array.from(graph.reverseEdges.get(packageName) || []);
13557
+ return {
13558
+ name: packageName,
13559
+ type: node.type,
13560
+ path: node.path || "N/A",
13561
+ version: node.version,
13562
+ dependencies,
13563
+ dependents
13564
+ };
13565
+ }
13566
+ // ../../src/dependency-graph/dist/formatters.js
13563
13567
  function formatGraph(graph, format) {
13564
13568
  switch (format) {
13565
13569
  case "mermaid":
@@ -13570,21 +13574,24 @@ function formatGraph(graph, format) {
13570
13574
  return formatAsTree(graph);
13571
13575
  case "json":
13572
13576
  default:
13573
- return JSON.stringify({
13574
- nodes: Array.from(graph.nodes.values()),
13575
- edges: graph.edges
13576
- }, null, 2);
13577
+ return formatAsJson(graph);
13577
13578
  }
13578
13579
  }
13580
+ function formatAsJson(graph) {
13581
+ return JSON.stringify({
13582
+ nodes: Array.from(graph.nodes.values()),
13583
+ edges: graph.edges
13584
+ }, null, 2);
13585
+ }
13579
13586
  function formatAsMermaid(graph) {
13580
13587
  const lines = ["graph TD"];
13581
13588
  for (const [name, node] of graph.nodes) {
13582
13589
  const label = node.type === "workspace" ? `\uD83D\uDCE6 ${name}` : `\uD83D\uDCDA ${name}`;
13583
- lines.push(` ${name.replace(/[^a-zA-Z0-9]/g, "_")}["${label}"]`);
13590
+ lines.push(` ${sanitizeId(name)}["${label}"]`);
13584
13591
  }
13585
13592
  for (const edge of graph.edges) {
13586
- const from = edge.from.replace(/[^a-zA-Z0-9]/g, "_");
13587
- const to = edge.to.replace(/[^a-zA-Z0-9]/g, "_");
13593
+ const from = sanitizeId(edge.from);
13594
+ const to = sanitizeId(edge.to);
13588
13595
  const label = edge.type === "workspace" ? "workspace" : edge.type === "import" ? "imports" : "external";
13589
13596
  lines.push(` ${from} -->|${label}| ${to}`);
13590
13597
  }
@@ -13592,7 +13599,7 @@ function formatAsMermaid(graph) {
13592
13599
  lines.push(" classDef external fill:#f5f5f5");
13593
13600
  lines.push(" classDef import fill:#fff3e0");
13594
13601
  for (const [name, node] of graph.nodes) {
13595
- const id = name.replace(/[^a-zA-Z0-9]/g, "_");
13602
+ const id = sanitizeId(name);
13596
13603
  if (node.type === "workspace") {
13597
13604
  lines.push(` class ${id} workspace`);
13598
13605
  } else if (node.type === "external") {
@@ -13651,8 +13658,8 @@ function printTree(graph, nodeName, prefix, seen, lines) {
13651
13658
  for (let i = 0;i < outgoing.length; i++) {
13652
13659
  const edge = outgoing[i];
13653
13660
  const isLast = i === outgoing.length - 1;
13654
- const connector = isLast ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
13655
- const childPrefix = prefix + (isLast ? " " : "\u2502 ");
13661
+ const connector = isLast ? "└──" : "├──";
13662
+ const childPrefix = prefix + (isLast ? " " : " ");
13656
13663
  lines.push(`${prefix}${connector} \uD83D\uDCE6 ${edge.to} [${edge.type}]`);
13657
13664
  if (!seen.has(edge.to)) {
13658
13665
  printTree(graph, edge.to, childPrefix, seen, lines);
@@ -13660,77 +13667,114 @@ function printTree(graph, nodeName, prefix, seen, lines) {
13660
13667
  }
13661
13668
  const externals = graph.edges.filter((e) => e.from === nodeName && graph.nodes.get(e.to)?.type === "external");
13662
13669
  if (externals.length > 0) {
13663
- lines.push(`${prefix}\u2514\u2500\u2500 \uD83D\uDCDA ${externals.length} external dependencies`);
13670
+ lines.push(`${prefix}└── \uD83D\uDCDA ${externals.length} external dependencies`);
13664
13671
  }
13665
13672
  }
13666
- function findCircularDependencies(graph, maxDepth = 10) {
13667
- const cycles = [];
13668
- const visited = new Set;
13669
- const recursionStack = new Set;
13670
- function dfs(node, path) {
13671
- if (path.length > maxDepth)
13672
- return;
13673
- visited.add(node);
13674
- recursionStack.add(node);
13675
- path.push(node);
13676
- for (const edge of graph.edges) {
13677
- if (edge.from === node) {
13678
- if (recursionStack.has(edge.to)) {
13679
- const cycleStart = path.indexOf(edge.to);
13680
- if (cycleStart >= 0) {
13681
- cycles.push([...path.slice(cycleStart), edge.to]);
13682
- }
13683
- } else if (!visited.has(edge.to)) {
13684
- dfs(edge.to, [...path]);
13685
- }
13673
+ function sanitizeId(name) {
13674
+ return name.replace(/[^a-zA-Z0-9]/g, "_");
13675
+ }
13676
+ // src/index.ts
13677
+ var DEPENDENCY_GRAPH_SCHEMA = {
13678
+ name: "dependency_graph",
13679
+ description: "Build a complete dependency graph of the monorepo",
13680
+ inputSchema: {
13681
+ type: "object",
13682
+ properties: {
13683
+ includeDevDependencies: {
13684
+ type: "boolean",
13685
+ description: "Include devDependencies in the graph",
13686
+ default: false
13687
+ },
13688
+ analyzeImports: {
13689
+ type: "boolean",
13690
+ description: "Analyze TypeScript/JavaScript imports",
13691
+ default: true
13692
+ },
13693
+ excludePatterns: {
13694
+ type: "array",
13695
+ items: { type: "string" },
13696
+ description: "Regex patterns to exclude from dependency analysis",
13697
+ default: []
13698
+ },
13699
+ format: {
13700
+ type: "string",
13701
+ enum: ["json", "mermaid", "dot", "tree"],
13702
+ description: "Output format for the graph",
13703
+ default: "json"
13686
13704
  }
13687
13705
  }
13688
- recursionStack.delete(node);
13689
13706
  }
13690
- for (const [name, node] of graph.nodes) {
13691
- if (node.type === "workspace" && !visited.has(name)) {
13692
- dfs(name, []);
13693
- }
13707
+ };
13708
+ var IMPACT_ANALYSIS_SCHEMA = {
13709
+ name: "impact_analysis",
13710
+ description: "Analyze the impact of changing a specific package",
13711
+ inputSchema: {
13712
+ type: "object",
13713
+ properties: {
13714
+ package: {
13715
+ type: "string",
13716
+ description: "Package name to analyze"
13717
+ },
13718
+ includeTransitive: {
13719
+ type: "boolean",
13720
+ description: "Include transitive dependents",
13721
+ default: true
13722
+ },
13723
+ format: {
13724
+ type: "string",
13725
+ enum: ["json", "tree"],
13726
+ description: "Output format",
13727
+ default: "tree"
13728
+ }
13729
+ },
13730
+ required: ["package"]
13694
13731
  }
13695
- return cycles;
13696
- }
13697
- function analyzeImpact(graph, packageName, includeTransitive = true) {
13698
- const direct = [];
13699
- const transitive = [];
13700
- const visited = new Set;
13701
- const directDependents = graph.reverseEdges.get(packageName);
13702
- if (directDependents) {
13703
- for (const dep of directDependents) {
13704
- direct.push(dep);
13705
- visited.add(dep);
13732
+ };
13733
+ var FIND_CIRCULAR_SCHEMA = {
13734
+ name: "find_circular",
13735
+ description: "Find circular dependencies in the monorepo",
13736
+ inputSchema: {
13737
+ type: "object",
13738
+ properties: {
13739
+ maxDepth: {
13740
+ type: "number",
13741
+ description: "Maximum depth to search for cycles",
13742
+ default: 10
13743
+ }
13706
13744
  }
13707
13745
  }
13708
- if (includeTransitive) {
13709
- for (const dep of direct) {
13710
- collectTransitive(graph, dep, visited, transitive);
13746
+ };
13747
+ var UNUSED_CODE_SCHEMA = {
13748
+ name: "unused_code",
13749
+ description: "Find potentially unused packages (no dependents)",
13750
+ inputSchema: {
13751
+ type: "object",
13752
+ properties: {
13753
+ includeExternal: {
13754
+ type: "boolean",
13755
+ description: "Include external dependencies",
13756
+ default: false
13757
+ }
13711
13758
  }
13712
13759
  }
13713
- return {
13714
- direct,
13715
- transitive,
13716
- all: Array.from(visited)
13717
- };
13718
- }
13719
- function collectTransitive(graph, packageName, visited, result) {
13720
- const dependents = graph.reverseEdges.get(packageName);
13721
- if (!dependents)
13722
- return;
13723
- for (const dep of dependents) {
13724
- if (!visited.has(dep)) {
13725
- visited.add(dep);
13726
- result.push(dep);
13727
- collectTransitive(graph, dep, visited, result);
13728
- }
13760
+ };
13761
+ var PACKAGE_INFO_SCHEMA = {
13762
+ name: "package_info",
13763
+ description: "Get detailed information about a specific package",
13764
+ inputSchema: {
13765
+ type: "object",
13766
+ properties: {
13767
+ package: {
13768
+ type: "string",
13769
+ description: "Package name"
13770
+ }
13771
+ },
13772
+ required: ["package"]
13729
13773
  }
13730
- }
13774
+ };
13731
13775
  var server = new Server({
13732
13776
  name: "@ebowwa/dependency-graph-mcp",
13733
- version: "1.0.0"
13777
+ version: "1.0.1"
13734
13778
  }, {
13735
13779
  capabilities: {
13736
13780
  tools: {}
@@ -13766,7 +13810,11 @@ server.setRequestHandler(CallToolRequestSchema2, async (request) => {
13766
13810
  excludePatterns = [],
13767
13811
  format = "json"
13768
13812
  } = args;
13769
- await cachedBuilder.build({ includeDevDependencies, analyzeImports, excludePatterns });
13813
+ await cachedBuilder.build({
13814
+ includeDevDependencies,
13815
+ analyzeImports,
13816
+ excludePatterns
13817
+ });
13770
13818
  const graph = cachedBuilder.getGraph();
13771
13819
  cachedGraph = graph;
13772
13820
  return {
@@ -13820,15 +13868,7 @@ Total affected: ${impact.all.length} packages`);
13820
13868
  }
13821
13869
  case "unused_code": {
13822
13870
  const { includeExternal = false } = args;
13823
- const unused = [];
13824
- for (const [name2, node] of cachedGraph.nodes) {
13825
- if (node.type === "workspace" || includeExternal) {
13826
- const dependents = cachedGraph.reverseEdges.get(name2);
13827
- if (!dependents || dependents.size === 0) {
13828
- unused.push(name2);
13829
- }
13830
- }
13831
- }
13871
+ const unused = findUnusedPackages(cachedGraph, includeExternal);
13832
13872
  if (unused.length === 0) {
13833
13873
  return {
13834
13874
  content: [{ type: "text", text: "No unused packages found!" }]
@@ -13848,28 +13888,30 @@ ${unused.map((n) => ` \u2514\u2500\u2500 ${n}`).join(`
13848
13888
  }
13849
13889
  case "package_info": {
13850
13890
  const { package: packageName } = args;
13851
- const node = cachedGraph.nodes.get(packageName);
13852
- if (!node) {
13891
+ const info = getPackageInfo(cachedGraph, packageName);
13892
+ if (!info) {
13853
13893
  return {
13854
- content: [{ type: "text", text: `Package "${packageName}" not found in dependency graph.` }]
13894
+ content: [
13895
+ {
13896
+ type: "text",
13897
+ text: `Package "${packageName}" not found in dependency graph.`
13898
+ }
13899
+ ]
13855
13900
  };
13856
13901
  }
13857
- const dependencies = cachedGraph.edges.filter((e) => e.from === packageName);
13858
- const dependents = Array.from(cachedGraph.reverseEdges.get(packageName) || []);
13859
13902
  const lines = [
13860
- `Package: ${packageName}`,
13861
- `Type: ${node.type}`,
13862
- `Path: ${node.path || "N/A"}`,
13863
- node.version ? `Version: ${node.version}` : "",
13903
+ `Package: ${info.name}`,
13904
+ `Type: ${info.type}`,
13905
+ `Path: ${info.path}`,
13906
+ info.version ? `Version: ${info.version}` : "",
13864
13907
  "",
13865
- `Dependencies (${dependencies.length}):`
13908
+ `Dependencies (${info.dependencies.length}):`
13866
13909
  ];
13867
- for (const dep of dependencies) {
13868
- const depNode = cachedGraph.nodes.get(dep.to);
13869
- lines.push(` \u2514\u2500\u2500 ${dep.to} [${dep.type}]${depNode?.version ? ` @ ${depNode.version}` : ""}`);
13910
+ for (const dep of info.dependencies) {
13911
+ lines.push(` \u2514\u2500\u2500 ${dep.name} [${dep.type}]${dep.version ? ` @ ${dep.version}` : ""}`);
13870
13912
  }
13871
- lines.push(``, `Dependents (${dependents.length}):`);
13872
- for (const dep of dependents) {
13913
+ lines.push(``, `Dependents (${info.dependents.length}):`);
13914
+ for (const dep of info.dependents) {
13873
13915
  lines.push(` \u2514\u2500\u2500 ${dep}`);
13874
13916
  }
13875
13917
  return { content: [{ type: "text", text: lines.join(`
@@ -13880,7 +13922,12 @@ ${unused.map((n) => ` \u2514\u2500\u2500 ${n}`).join(`
13880
13922
  }
13881
13923
  } catch (error2) {
13882
13924
  return {
13883
- content: [{ type: "text", text: `Error: ${error2 instanceof Error ? error2.message : String(error2)}` }],
13925
+ content: [
13926
+ {
13927
+ type: "text",
13928
+ text: `Error: ${error2 instanceof Error ? error2.message : String(error2)}`
13929
+ }
13930
+ ],
13884
13931
  isError: true
13885
13932
  };
13886
13933
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ebowwa/dependency-graph-mcp",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "description": "MCP server for dependency graph analysis and visualization in monorepos",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -11,9 +11,14 @@
11
11
  "scripts": {
12
12
  "build": "bun build src/cli.ts --outdir dist --target node && bun build src/index.ts --outdir dist --target node && chmod +x dist/cli.js",
13
13
  "dev": "bun run src/index.ts",
14
- "mcp-dev": "MCP_DEV=true bun run src/index.ts"
14
+ "mcp-dev": "MCP_DEV=true bun run src/index.ts",
15
+ "bump:patch": "npm version patch --no-git-tag-version && bun run build",
16
+ "bump:minor": "npm version minor --no-git-tag-version && bun run build",
17
+ "bump:major": "npm version major --no-git-tag-version && bun run build",
18
+ "prepublishOnly": "bun run build"
15
19
  },
16
20
  "dependencies": {
21
+ "@ebowwa/dependency-graph": "^1.0.2",
17
22
  "@modelcontextprotocol/sdk": "^1.0.4",
18
23
  "zod": "^3.24.1"
19
24
  },
package/src/index.ts CHANGED
@@ -1,14 +1,9 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * Dependency Graph MCP Server
3
+ * @ebowwa/dependency-graph-mcp
4
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
5
+ * MCP interface layer for dependency graph analysis.
6
+ * Uses @ebowwa/dependency-graph for core logic.
12
7
  */
13
8
 
14
9
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -17,351 +12,21 @@ import {
17
12
  CallToolRequestSchema,
18
13
  ListToolsRequestSchema,
19
14
  } 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
15
 
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
- }
16
+ // Import from core package
17
+ import {
18
+ DependencyGraphBuilder,
19
+ formatGraph,
20
+ findCircularDependencies,
21
+ analyzeImpact,
22
+ findUnusedPackages,
23
+ getPackageInfo,
24
+ type DependencyGraph,
25
+ type OutputFormat,
26
+ } from "@ebowwa/dependency-graph";
362
27
 
363
28
  // ==============
364
- // MCP Server
29
+ // MCP Tool Schemas
365
30
  // ==============
366
31
 
367
32
  const DEPENDENCY_GRAPH_SCHEMA = {
@@ -467,242 +132,14 @@ const PACKAGE_INFO_SCHEMA = {
467
132
  },
468
133
  };
469
134
 
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
- }
135
+ // ==============
136
+ // MCP Server
137
+ // ==============
700
138
 
701
- // Start the MCP server
702
139
  const server = new Server(
703
140
  {
704
141
  name: "@ebowwa/dependency-graph-mcp",
705
- version: "1.0.0",
142
+ version: "1.0.1",
706
143
  },
707
144
  {
708
145
  capabilities: {
@@ -753,10 +190,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
753
190
  includeDevDependencies?: boolean;
754
191
  analyzeImports?: boolean;
755
192
  excludePatterns?: string[];
756
- format?: "json" | "mermaid" | "dot" | "tree";
193
+ format?: OutputFormat;
757
194
  };
758
195
 
759
- await cachedBuilder.build({ includeDevDependencies, analyzeImports, excludePatterns });
196
+ await cachedBuilder.build({
197
+ includeDevDependencies,
198
+ analyzeImports,
199
+ excludePatterns,
200
+ });
760
201
  const graph = cachedBuilder.getGraph();
761
202
  cachedGraph = graph;
762
203
 
@@ -766,11 +207,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
766
207
  }
767
208
 
768
209
  case "impact_analysis": {
769
- const { package: packageName, includeTransitive = true, format = "tree" } = args as {
770
- package: string;
771
- includeTransitive?: boolean;
772
- format?: "json" | "tree";
773
- };
210
+ const { package: packageName, includeTransitive = true, format = "tree" } =
211
+ args as {
212
+ package: string;
213
+ includeTransitive?: boolean;
214
+ format?: "json" | "tree";
215
+ };
774
216
 
775
217
  const impact = analyzeImpact(cachedGraph, packageName, includeTransitive);
776
218
 
@@ -819,16 +261,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
819
261
  case "unused_code": {
820
262
  const { includeExternal = false } = args as { includeExternal?: boolean };
821
263
 
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
- }
264
+ const unused = findUnusedPackages(cachedGraph, includeExternal);
832
265
 
833
266
  if (unused.length === 0) {
834
267
  return {
@@ -840,7 +273,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
840
273
  content: [
841
274
  {
842
275
  type: "text",
843
- text: `Found ${unused.length} potentially unused packages:\n\n${unused.map(n => ` └── ${n}`).join("\n")}`,
276
+ text: `Found ${unused.length} potentially unused packages:\n\n${unused.map((n) => ` └── ${n}`).join("\n")}`,
844
277
  },
845
278
  ],
846
279
  };
@@ -849,34 +282,35 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
849
282
  case "package_info": {
850
283
  const { package: packageName } = args as { package: string };
851
284
 
852
- const node = cachedGraph.nodes.get(packageName);
853
- if (!node) {
285
+ const info = getPackageInfo(cachedGraph, packageName);
286
+ if (!info) {
854
287
  return {
855
- content: [{ type: "text", text: `Package "${packageName}" not found in dependency graph.` }],
288
+ content: [
289
+ {
290
+ type: "text",
291
+ text: `Package "${packageName}" not found in dependency graph.`,
292
+ },
293
+ ],
856
294
  };
857
295
  }
858
296
 
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
297
  const lines = [
865
- `Package: ${packageName}`,
866
- `Type: ${node.type}`,
867
- `Path: ${node.path || "N/A"}`,
868
- node.version ? `Version: ${node.version}` : "",
298
+ `Package: ${info.name}`,
299
+ `Type: ${info.type}`,
300
+ `Path: ${info.path}`,
301
+ info.version ? `Version: ${info.version}` : "",
869
302
  "",
870
- `Dependencies (${dependencies.length}):`,
303
+ `Dependencies (${info.dependencies.length}):`,
871
304
  ];
872
305
 
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}` : ""}`);
306
+ for (const dep of info.dependencies) {
307
+ lines.push(
308
+ ` └── ${dep.name} [${dep.type}]${dep.version ? ` @ ${dep.version}` : ""}`
309
+ );
876
310
  }
877
311
 
878
- lines.push(``, `Dependents (${dependents.length}):`);
879
- for (const dep of dependents) {
312
+ lines.push(``, `Dependents (${info.dependents.length}):`);
313
+ for (const dep of info.dependents) {
880
314
  lines.push(` └── ${dep}`);
881
315
  }
882
316
 
@@ -888,7 +322,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
888
322
  }
889
323
  } catch (error) {
890
324
  return {
891
- content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
325
+ content: [
326
+ {
327
+ type: "text",
328
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
329
+ },
330
+ ],
892
331
  isError: true,
893
332
  };
894
333
  }
@@ -897,17 +336,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
897
336
  // Start server
898
337
  async function main() {
899
338
  // Set up error handling
900
- server.onerror = (error) => console.error('[MCP] Error:', error);
339
+ server.onerror = (error) => console.error("[MCP] Error:", error);
901
340
 
902
341
  // Signal handlers for graceful shutdown
903
- process.on('SIGINT', async () => {
904
- console.error('[MCP] Shutting down...');
342
+ process.on("SIGINT", async () => {
343
+ console.error("[MCP] Shutting down...");
905
344
  await server.close();
906
345
  process.exit(0);
907
346
  });
908
347
 
909
- process.on('SIGTERM', async () => {
910
- console.error('[MCP] Shutting down...');
348
+ process.on("SIGTERM", async () => {
349
+ console.error("[MCP] Shutting down...");
911
350
  await server.close();
912
351
  process.exit(0);
913
352
  });
@@ -917,13 +356,13 @@ async function main() {
917
356
  await server.connect(transport);
918
357
 
919
358
  // Log to stderr (doesn't interfere with JSON-RPC)
920
- console.error('[MCP] @ebowwa/dependency-graph-mcp server running on stdio');
359
+ console.error("[MCP] @ebowwa/dependency-graph-mcp server running on stdio");
921
360
 
922
361
  // Keep stdin open for requests
923
362
  process.stdin.resume();
924
363
  }
925
364
 
926
365
  main().catch((error) => {
927
- console.error('[MCP] Fatal error:', error);
366
+ console.error("[MCP] Fatal error:", error);
928
367
  process.exit(1);
929
368
  });