depgraph-core 1.8.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/depgraph-output.json +1133 -84
- package/README.md +6 -6
- package/depgraph-mcp.js +180 -40
- package/depgraph.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -311,18 +311,18 @@ DepGraph ships an **MCP server** (`depgraph-mcp`) that lets Claude analyze your
|
|
|
311
311
|
|
|
312
312
|
### Quick setup
|
|
313
313
|
|
|
314
|
-
**
|
|
314
|
+
**If the package is published to npm** (zero-install, always up to date):
|
|
315
315
|
```bash
|
|
316
|
-
|
|
317
|
-
npm run release:all # compiles + bundles both CLI and MCP server
|
|
316
|
+
claude mcp add depgraph -- npx -p depgraph-core depgraph-mcp
|
|
318
317
|
```
|
|
319
318
|
|
|
320
|
-
**
|
|
319
|
+
**If running from a local build** (clone the repo first):
|
|
321
320
|
```bash
|
|
322
|
-
|
|
321
|
+
npm install && npm run release:all
|
|
322
|
+
npm run mcp:register
|
|
323
323
|
```
|
|
324
324
|
|
|
325
|
-
**
|
|
325
|
+
**Then use it** — open Claude Code in any project and ask naturally:
|
|
326
326
|
```
|
|
327
327
|
Summarize the dependency graph of /Users/me/my-app
|
|
328
328
|
What is the risk of changing getUserById in /Users/me/my-app?
|
package/depgraph-mcp.js
CHANGED
|
@@ -24110,28 +24110,79 @@ function buildPipeline(projectDir) {
|
|
|
24110
24110
|
const metrics = computeMetrics(graph);
|
|
24111
24111
|
return { files, parsed, graph, metrics };
|
|
24112
24112
|
}
|
|
24113
|
-
function
|
|
24113
|
+
function buildSubgraph(graph, focusName, maxDepth) {
|
|
24114
|
+
const focusNode = [...graph.nodes.values()].find((n) => n.name === focusName);
|
|
24115
|
+
if (!focusNode) return { nodes: /* @__PURE__ */ new Map(), edges: [] };
|
|
24116
|
+
const included = /* @__PURE__ */ new Set();
|
|
24117
|
+
const queue = [{ id: focusNode.id, depth: 0 }];
|
|
24118
|
+
while (queue.length > 0) {
|
|
24119
|
+
const { id, depth } = queue.shift();
|
|
24120
|
+
if (included.has(id)) continue;
|
|
24121
|
+
included.add(id);
|
|
24122
|
+
if (depth >= maxDepth) continue;
|
|
24123
|
+
for (const edge of graph.edges) {
|
|
24124
|
+
if (edge.from === id && !included.has(edge.to)) queue.push({ id: edge.to, depth: depth + 1 });
|
|
24125
|
+
if (edge.to === id && !included.has(edge.from)) queue.push({ id: edge.from, depth: depth + 1 });
|
|
24126
|
+
}
|
|
24127
|
+
}
|
|
24128
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
24129
|
+
for (const id of included) {
|
|
24130
|
+
const node = graph.nodes.get(id);
|
|
24131
|
+
if (node) nodes.set(id, node);
|
|
24132
|
+
}
|
|
24133
|
+
return {
|
|
24134
|
+
nodes,
|
|
24135
|
+
edges: graph.edges.filter((e) => included.has(e.from) && included.has(e.to))
|
|
24136
|
+
};
|
|
24137
|
+
}
|
|
24138
|
+
function buildProseSummary(summary) {
|
|
24139
|
+
const s = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
24140
|
+
const shortIds = (ids) => ids.slice(0, 3).map((id) => id.split("__")[0]).join(", ") + (ids.length > 3 ? ` (+${ids.length - 3} more)` : "");
|
|
24141
|
+
const parts = [
|
|
24142
|
+
`${s(summary.totalFiles, "file")}, ${s(summary.totalNodes, "node")}, ${s(summary.totalEdges, "edge")}.`
|
|
24143
|
+
];
|
|
24144
|
+
if (summary.criticalNodes.length > 0) {
|
|
24145
|
+
parts.push(
|
|
24146
|
+
`${s(summary.criticalNodes.length, "critical node")} \u2014 change carefully: ${shortIds(summary.criticalNodes)}.`
|
|
24147
|
+
);
|
|
24148
|
+
}
|
|
24149
|
+
if (summary.entryPoints.length > 0) {
|
|
24150
|
+
parts.push(`${s(summary.entryPoints.length, "entry point")}: ${shortIds(summary.entryPoints)}.`);
|
|
24151
|
+
}
|
|
24152
|
+
if (summary.leafNodes.length > 0) {
|
|
24153
|
+
parts.push(`${s(summary.leafNodes.length, "leaf node")} (pure utilities with no outgoing deps).`);
|
|
24154
|
+
}
|
|
24155
|
+
if (summary.isolatedNodes.length > 0) {
|
|
24156
|
+
parts.push(`${s(summary.isolatedNodes.length, "isolated node")} \u2014 potential dead code: ${shortIds(summary.isolatedNodes)}.`);
|
|
24157
|
+
}
|
|
24158
|
+
return parts.join(" ");
|
|
24159
|
+
}
|
|
24160
|
+
function analyzeProject(projectDir, opts = {}) {
|
|
24161
|
+
const { verbosity = "full", focus, depth = 3 } = opts;
|
|
24114
24162
|
const { files, parsed, metrics } = buildPipeline(projectDir);
|
|
24115
24163
|
const totalLines = parsed.reduce((sum, f) => sum + f.lines, 0);
|
|
24116
|
-
|
|
24117
|
-
|
|
24118
|
-
|
|
24119
|
-
|
|
24120
|
-
|
|
24121
|
-
|
|
24122
|
-
|
|
24123
|
-
|
|
24124
|
-
|
|
24125
|
-
|
|
24126
|
-
|
|
24127
|
-
|
|
24128
|
-
|
|
24129
|
-
|
|
24130
|
-
},
|
|
24131
|
-
nodes: [...metrics.nodes.values()],
|
|
24132
|
-
edges: metrics.edges,
|
|
24133
|
-
files: parsed
|
|
24164
|
+
const graph = focus ? buildSubgraph(metrics, focus, depth) : metrics;
|
|
24165
|
+
const meta = {
|
|
24166
|
+
version: VERSION,
|
|
24167
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
24168
|
+
totalFiles: parsed.length,
|
|
24169
|
+
totalLines
|
|
24170
|
+
};
|
|
24171
|
+
const summary = {
|
|
24172
|
+
totalNodes: graph.nodes.size,
|
|
24173
|
+
totalEdges: graph.edges.length,
|
|
24174
|
+
entryPoints: getEntryPoints(graph),
|
|
24175
|
+
leafNodes: getLeafNodes(graph),
|
|
24176
|
+
isolatedNodes: getIsolatedNodes(graph),
|
|
24177
|
+
criticalNodes: getCriticalNodes(graph)
|
|
24134
24178
|
};
|
|
24179
|
+
if (verbosity === "sketch") {
|
|
24180
|
+
return { meta, summary, nodes: [], edges: [], files: [] };
|
|
24181
|
+
}
|
|
24182
|
+
if (verbosity === "overview") {
|
|
24183
|
+
return { meta, summary, nodes: [...graph.nodes.values()], edges: [], files: [] };
|
|
24184
|
+
}
|
|
24185
|
+
return { meta, summary, nodes: [...graph.nodes.values()], edges: graph.edges, files: parsed };
|
|
24135
24186
|
}
|
|
24136
24187
|
function analyzeImpact(projectDir, targetNode, changeDescription) {
|
|
24137
24188
|
const { metrics } = buildPipeline(projectDir);
|
|
@@ -24149,16 +24200,68 @@ function analyzeGitImpact(options) {
|
|
|
24149
24200
|
}));
|
|
24150
24201
|
return { changedEntities, impacts };
|
|
24151
24202
|
}
|
|
24152
|
-
function getGraphSummary(projectDir) {
|
|
24203
|
+
function getGraphSummary(projectDir, verbosity = "overview") {
|
|
24153
24204
|
const { files, parsed, metrics } = buildPipeline(projectDir);
|
|
24154
|
-
|
|
24205
|
+
const base = {
|
|
24155
24206
|
totalFiles: files.length,
|
|
24156
24207
|
totalNodes: metrics.nodes.size,
|
|
24157
24208
|
totalEdges: metrics.edges.length,
|
|
24209
|
+
criticalNodes: getCriticalNodes(metrics)
|
|
24210
|
+
};
|
|
24211
|
+
if (verbosity === "sketch") {
|
|
24212
|
+
return { ...base, entryPoints: [], leafNodes: [], isolatedNodes: [] };
|
|
24213
|
+
}
|
|
24214
|
+
return {
|
|
24215
|
+
...base,
|
|
24158
24216
|
entryPoints: getEntryPoints(metrics),
|
|
24159
24217
|
leafNodes: getLeafNodes(metrics),
|
|
24160
|
-
isolatedNodes: getIsolatedNodes(metrics)
|
|
24161
|
-
|
|
24218
|
+
isolatedNodes: getIsolatedNodes(metrics)
|
|
24219
|
+
};
|
|
24220
|
+
}
|
|
24221
|
+
function describeNode(projectDir, nodeName) {
|
|
24222
|
+
const { metrics } = buildPipeline(projectDir);
|
|
24223
|
+
const node = [...metrics.nodes.values()].find((n) => n.name === nodeName);
|
|
24224
|
+
if (!node) {
|
|
24225
|
+
return {
|
|
24226
|
+
found: false,
|
|
24227
|
+
name: nodeName,
|
|
24228
|
+
file: "",
|
|
24229
|
+
line: 0,
|
|
24230
|
+
type: "",
|
|
24231
|
+
lang: "",
|
|
24232
|
+
centralityScore: 0,
|
|
24233
|
+
inDegree: 0,
|
|
24234
|
+
outDegree: 0,
|
|
24235
|
+
importedBy: [],
|
|
24236
|
+
imports: [],
|
|
24237
|
+
role: "not found"
|
|
24238
|
+
};
|
|
24239
|
+
}
|
|
24240
|
+
const importedBy = [...new Set(
|
|
24241
|
+
metrics.edges.filter((e) => e.to === node.id).map((e) => metrics.nodes.get(e.from)?.name).filter((n) => Boolean(n))
|
|
24242
|
+
)];
|
|
24243
|
+
const imports = [...new Set(
|
|
24244
|
+
metrics.edges.filter((e) => e.from === node.id).map((e) => metrics.nodes.get(e.to)?.name).filter((n) => Boolean(n))
|
|
24245
|
+
)];
|
|
24246
|
+
let role;
|
|
24247
|
+
if (node.centralityScore > 20) role = "critical shared dependency";
|
|
24248
|
+
else if (node.inDegree === 0 && node.outDegree > 0) role = "entry point";
|
|
24249
|
+
else if (node.outDegree === 0 && node.inDegree > 0) role = "leaf";
|
|
24250
|
+
else if (node.inDegree === 0 && node.outDegree === 0) role = "isolated \u2014 potential dead code";
|
|
24251
|
+
else role = "connector";
|
|
24252
|
+
return {
|
|
24253
|
+
found: true,
|
|
24254
|
+
name: node.name,
|
|
24255
|
+
file: node.file,
|
|
24256
|
+
line: node.line,
|
|
24257
|
+
type: node.type,
|
|
24258
|
+
lang: node.lang,
|
|
24259
|
+
centralityScore: node.centralityScore,
|
|
24260
|
+
inDegree: node.inDegree,
|
|
24261
|
+
outDegree: node.outDegree,
|
|
24262
|
+
importedBy,
|
|
24263
|
+
imports,
|
|
24264
|
+
role
|
|
24162
24265
|
};
|
|
24163
24266
|
}
|
|
24164
24267
|
|
|
@@ -24168,16 +24271,17 @@ var server = new McpServer({
|
|
|
24168
24271
|
version: VERSION
|
|
24169
24272
|
});
|
|
24170
24273
|
server.tool(
|
|
24171
|
-
"
|
|
24172
|
-
|
|
24274
|
+
"describe_node",
|
|
24275
|
+
'PREFERRED first tool for questions about a specific function, class, or entity. Returns what depends on it, what it depends on, its file/line, type, and role \u2014 all in ~50 tokens. Use this INSTEAD of analyze_project when the question is "tell me about X" or "what uses X" or "what does X import". Much cheaper than loading the full graph.',
|
|
24173
24276
|
{
|
|
24174
|
-
projectDir: external_exports.string().describe(
|
|
24175
|
-
|
|
24277
|
+
projectDir: external_exports.string().describe("Absolute path to the project directory"),
|
|
24278
|
+
nodeName: external_exports.string().describe(
|
|
24279
|
+
'Exact name of the entity to look up (e.g. "getUserById", "UserService", "AuthMiddleware")'
|
|
24176
24280
|
)
|
|
24177
24281
|
},
|
|
24178
|
-
async ({ projectDir }) => {
|
|
24282
|
+
async ({ projectDir, nodeName }) => {
|
|
24179
24283
|
try {
|
|
24180
|
-
const result =
|
|
24284
|
+
const result = describeNode(projectDir, nodeName);
|
|
24181
24285
|
return {
|
|
24182
24286
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
24183
24287
|
};
|
|
@@ -24191,15 +24295,22 @@ server.tool(
|
|
|
24191
24295
|
);
|
|
24192
24296
|
server.tool(
|
|
24193
24297
|
"get_graph_summary",
|
|
24194
|
-
|
|
24298
|
+
'Get a project-wide overview \u2014 file/node/edge counts, critical nodes, entry points, leaf nodes, and isolated nodes. Use verbosity "sketch" (~50 tokens) for a quick orientation, "overview" for the full summary. Use format "prose" to get a single readable sentence (~100 tokens) instead of JSON. Call this before analyze_project or when you only need counts and structure, not full node data.',
|
|
24195
24299
|
{
|
|
24196
|
-
projectDir: external_exports.string().describe("Absolute path to the project directory")
|
|
24300
|
+
projectDir: external_exports.string().describe("Absolute path to the project directory"),
|
|
24301
|
+
verbosity: external_exports.enum(["sketch", "overview", "full"]).optional().describe(
|
|
24302
|
+
'"sketch" = counts + critical nodes only (~50 tokens). "overview" = full summary with entry/leaf/isolated lists (default). "full" = same as overview.'
|
|
24303
|
+
),
|
|
24304
|
+
format: external_exports.enum(["json", "prose"]).optional().describe(
|
|
24305
|
+
'"json" = structured JSON object (default). "prose" = single readable paragraph (~100 tokens) \u2014 use when you want compact context without JSON overhead.'
|
|
24306
|
+
)
|
|
24197
24307
|
},
|
|
24198
|
-
async ({ projectDir }) => {
|
|
24308
|
+
async ({ projectDir, verbosity, format }) => {
|
|
24199
24309
|
try {
|
|
24200
|
-
const result = getGraphSummary(projectDir);
|
|
24310
|
+
const result = getGraphSummary(projectDir, verbosity ?? "overview");
|
|
24311
|
+
const text = format === "prose" ? buildProseSummary(result) : JSON.stringify(result, null, 2);
|
|
24201
24312
|
return {
|
|
24202
|
-
content: [{ type: "text", text
|
|
24313
|
+
content: [{ type: "text", text }]
|
|
24203
24314
|
};
|
|
24204
24315
|
} catch (err) {
|
|
24205
24316
|
return {
|
|
@@ -24211,16 +24322,14 @@ server.tool(
|
|
|
24211
24322
|
);
|
|
24212
24323
|
server.tool(
|
|
24213
24324
|
"simulate_impact",
|
|
24214
|
-
|
|
24325
|
+
'PREFERRED tool when the question is impact-oriented: "what breaks if I change X?". Use this INSTEAD of analyze_project \u2014 it requires no graph preload and costs ~200 tokens vs ~80k for the full graph. Returns risk score (0-100), risk level (LOW/MEDIUM/HIGH/CRITICAL), all affected nodes, a testing plan, and recommendations.',
|
|
24215
24326
|
{
|
|
24216
|
-
projectDir: external_exports.string().describe(
|
|
24217
|
-
"Absolute path to the project directory"
|
|
24218
|
-
),
|
|
24327
|
+
projectDir: external_exports.string().describe("Absolute path to the project directory"),
|
|
24219
24328
|
targetNode: external_exports.string().describe(
|
|
24220
24329
|
'Name of the function, class, or entity to simulate changing (e.g. "getUserById")'
|
|
24221
24330
|
),
|
|
24222
24331
|
changeDescription: external_exports.string().describe(
|
|
24223
|
-
'
|
|
24332
|
+
'Description of the proposed change (e.g. "removing the userId parameter")'
|
|
24224
24333
|
)
|
|
24225
24334
|
},
|
|
24226
24335
|
async ({ projectDir, targetNode, changeDescription }) => {
|
|
@@ -24237,9 +24346,40 @@ server.tool(
|
|
|
24237
24346
|
}
|
|
24238
24347
|
}
|
|
24239
24348
|
);
|
|
24349
|
+
server.tool(
|
|
24350
|
+
"analyze_project",
|
|
24351
|
+
'Return the dependency graph. EXPENSIVE at full verbosity \u2014 use verbosity "sketch" or "overview", or narrow scope with "focus" + "depth" to avoid token overload. Prefer describe_node for single-entity questions, simulate_impact for risk questions, and get_graph_summary for project-wide orientation.',
|
|
24352
|
+
{
|
|
24353
|
+
projectDir: external_exports.string().describe(
|
|
24354
|
+
"Absolute path to the project directory to analyze (e.g. /Users/me/my-app)"
|
|
24355
|
+
),
|
|
24356
|
+
verbosity: external_exports.enum(["sketch", "overview", "full"]).optional().describe(
|
|
24357
|
+
'"sketch" = meta + summary only, no nodes/edges (~100 tokens). "overview" = meta + summary + nodes, no edges. "full" = complete graph incl. edges and file detail (default, can be very large).'
|
|
24358
|
+
),
|
|
24359
|
+
focus: external_exports.string().optional().describe(
|
|
24360
|
+
"Entity name to centre the graph on \u2014 returns only the subgraph within `depth` hops of this node (bidirectional). Use to scope the result to one module instead of the whole project."
|
|
24361
|
+
),
|
|
24362
|
+
depth: external_exports.number().optional().describe(
|
|
24363
|
+
"Max BFS hops from the focus node (default 3). Only used when focus is set."
|
|
24364
|
+
)
|
|
24365
|
+
},
|
|
24366
|
+
async ({ projectDir, verbosity, focus, depth }) => {
|
|
24367
|
+
try {
|
|
24368
|
+
const result = analyzeProject(projectDir, { verbosity, focus, depth });
|
|
24369
|
+
return {
|
|
24370
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
24371
|
+
};
|
|
24372
|
+
} catch (err) {
|
|
24373
|
+
return {
|
|
24374
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
24375
|
+
isError: true
|
|
24376
|
+
};
|
|
24377
|
+
}
|
|
24378
|
+
}
|
|
24379
|
+
);
|
|
24240
24380
|
server.tool(
|
|
24241
24381
|
"git_impact",
|
|
24242
|
-
"
|
|
24382
|
+
'PREFERRED tool for "what did this commit/branch change and what does it break?". Detects changed functions/classes from a git diff and simulates their downstream impact \u2014 no manual entity name needed. Supports uncommitted changes, a specific commit SHA, or a branch comparison.',
|
|
24243
24383
|
{
|
|
24244
24384
|
projectDir: external_exports.string().describe(
|
|
24245
24385
|
"Absolute path to the project directory (must be a git repository)"
|
package/depgraph.js
CHANGED
|
@@ -4173,7 +4173,7 @@ function getFlag(flag) {
|
|
|
4173
4173
|
}
|
|
4174
4174
|
function printHelp() {
|
|
4175
4175
|
console.log(`
|
|
4176
|
-
${bold("DepGraph")} ${dim("v1.
|
|
4176
|
+
${bold("DepGraph")} ${dim("v1.9.0")}
|
|
4177
4177
|
${dim("Dependency mapping \xB7 Impact simulation \xB7 Developer intelligence")}
|
|
4178
4178
|
|
|
4179
4179
|
${bold("USAGE")}
|
|
@@ -4222,7 +4222,7 @@ ${bold("GIT EXAMPLES")}
|
|
|
4222
4222
|
function printBanner() {
|
|
4223
4223
|
console.log(`
|
|
4224
4224
|
${bold("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501")}
|
|
4225
|
-
${bold(" DepGraph")} ${dim("v1.
|
|
4225
|
+
${bold(" DepGraph")} ${dim("v1.9.0")}
|
|
4226
4226
|
${bold("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501")}
|
|
4227
4227
|
`);
|
|
4228
4228
|
}
|