@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 +3 -0
- package/dist/index.js +234 -187
- package/package.json +7 -2
- package/src/index.ts +65 -626
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/
|
|
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
|
-
|
|
13466
|
-
|
|
13467
|
-
|
|
13468
|
-
|
|
13469
|
-
|
|
13470
|
-
|
|
13471
|
-
|
|
13472
|
-
|
|
13473
|
-
|
|
13474
|
-
|
|
13475
|
-
|
|
13476
|
-
|
|
13477
|
-
|
|
13478
|
-
|
|
13479
|
-
|
|
13480
|
-
|
|
13481
|
-
|
|
13482
|
-
|
|
13483
|
-
|
|
13484
|
-
|
|
13485
|
-
|
|
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
|
-
|
|
13497
|
-
|
|
13498
|
-
|
|
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
|
-
|
|
13522
|
-
|
|
13523
|
-
|
|
13524
|
-
|
|
13525
|
-
|
|
13526
|
-
|
|
13527
|
-
|
|
13528
|
-
|
|
13529
|
-
|
|
13530
|
-
|
|
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
|
-
|
|
13536
|
-
|
|
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
|
-
|
|
13550
|
-
|
|
13551
|
-
|
|
13552
|
-
|
|
13553
|
-
|
|
13554
|
-
|
|
13555
|
-
|
|
13556
|
-
|
|
13557
|
-
|
|
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
|
|
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
|
|
13590
|
+
lines.push(` ${sanitizeId(name)}["${label}"]`);
|
|
13584
13591
|
}
|
|
13585
13592
|
for (const edge of graph.edges) {
|
|
13586
|
-
const from = edge.from
|
|
13587
|
-
const to = edge.to
|
|
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
|
|
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 ? "
|
|
13655
|
-
const childPrefix = prefix + (isLast ? " " : "
|
|
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}
|
|
13670
|
+
lines.push(`${prefix}└── \uD83D\uDCDA ${externals.length} external dependencies`);
|
|
13664
13671
|
}
|
|
13665
13672
|
}
|
|
13666
|
-
function
|
|
13667
|
-
|
|
13668
|
-
|
|
13669
|
-
|
|
13670
|
-
|
|
13671
|
-
|
|
13672
|
-
|
|
13673
|
-
|
|
13674
|
-
|
|
13675
|
-
|
|
13676
|
-
|
|
13677
|
-
|
|
13678
|
-
|
|
13679
|
-
|
|
13680
|
-
|
|
13681
|
-
|
|
13682
|
-
|
|
13683
|
-
|
|
13684
|
-
|
|
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
|
-
|
|
13691
|
-
|
|
13692
|
-
|
|
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
|
-
|
|
13696
|
-
|
|
13697
|
-
|
|
13698
|
-
|
|
13699
|
-
|
|
13700
|
-
|
|
13701
|
-
|
|
13702
|
-
|
|
13703
|
-
|
|
13704
|
-
|
|
13705
|
-
|
|
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
|
-
|
|
13709
|
-
|
|
13710
|
-
|
|
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
|
-
|
|
13714
|
-
|
|
13715
|
-
|
|
13716
|
-
|
|
13717
|
-
|
|
13718
|
-
|
|
13719
|
-
|
|
13720
|
-
|
|
13721
|
-
|
|
13722
|
-
|
|
13723
|
-
|
|
13724
|
-
|
|
13725
|
-
|
|
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.
|
|
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({
|
|
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
|
|
13852
|
-
if (!
|
|
13891
|
+
const info = getPackageInfo(cachedGraph, packageName);
|
|
13892
|
+
if (!info) {
|
|
13853
13893
|
return {
|
|
13854
|
-
content: [
|
|
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: ${
|
|
13861
|
-
`Type: ${
|
|
13862
|
-
`Path: ${
|
|
13863
|
-
|
|
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
|
-
|
|
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: [
|
|
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.
|
|
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
|
-
*
|
|
3
|
+
* @ebowwa/dependency-graph-mcp
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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
|
-
//
|
|
471
|
-
|
|
472
|
-
|
|
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.
|
|
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?:
|
|
193
|
+
format?: OutputFormat;
|
|
757
194
|
};
|
|
758
195
|
|
|
759
|
-
await cachedBuilder.build({
|
|
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" } =
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
|
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
|
|
853
|
-
if (!
|
|
285
|
+
const info = getPackageInfo(cachedGraph, packageName);
|
|
286
|
+
if (!info) {
|
|
854
287
|
return {
|
|
855
|
-
content: [
|
|
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: ${
|
|
866
|
-
`Type: ${
|
|
867
|
-
`Path: ${
|
|
868
|
-
|
|
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
|
-
|
|
875
|
-
|
|
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: [
|
|
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(
|
|
339
|
+
server.onerror = (error) => console.error("[MCP] Error:", error);
|
|
901
340
|
|
|
902
341
|
// Signal handlers for graceful shutdown
|
|
903
|
-
process.on(
|
|
904
|
-
console.error(
|
|
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(
|
|
910
|
-
console.error(
|
|
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(
|
|
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(
|
|
366
|
+
console.error("[MCP] Fatal error:", error);
|
|
928
367
|
process.exit(1);
|
|
929
368
|
});
|