Skip to main content
Glama
cbunting99

MCP Code Analysis & Quality Server

by cbunting99
DependencyGraphBuilder.ts8.42 kB
// Copyright 2025 Chris Bunting // Brief: Builds dependency graphs from package manager data // Scope: Creates and manages dependency tree structures for analysis import { DependencyInfo, DependencyType, PackageManager } from '@mcp-code-analysis/shared-types'; import { Logger } from '../utils/Logger.js'; import graphlib from 'graphlib'; export interface DependencyNode { name: string; version: string; type: DependencyType; manager: PackageManager; depth: number; parent?: string; metadata: { licenses: string[]; vulnerabilities: any[]; size?: number; downloadCount?: number; lastUpdated?: string; }; } export interface DependencyEdge { from: string; to: string; type: 'direct' | 'transitive'; versionConstraint?: string; } export interface DependencyGraph { nodes: Map<string, DependencyNode>; edges: DependencyEdge[]; graph: graphlib.Graph; root: string; } export class DependencyGraphBuilder { private logger: Logger; constructor(logger?: Logger) { this.logger = logger || new Logger(); } async buildGraph( rootPackage: string, rootVersion: string, dependencies: DependencyInfo[], manager: PackageManager ): Promise<DependencyGraph> { this.logger.debug(`Building dependency graph for ${rootPackage}@${rootVersion}`); const graph = new graphlib.Graph({ directed: true }); const nodes = new Map<string, DependencyNode>(); const edges: DependencyEdge[] = []; // Add root node const rootNode: DependencyNode = { name: rootPackage, version: rootVersion, type: DependencyType.PRODUCTION, manager, depth: 0, metadata: { licenses: [], vulnerabilities: [], }, }; nodes.set(this.getNodeId(rootPackage, rootVersion), rootNode); graph.setNode(this.getNodeId(rootPackage, rootVersion), rootNode); // Process dependencies recursively await this.processDependencies( rootPackage, rootVersion, dependencies, manager, 1, nodes, edges, graph ); const dependencyGraph: DependencyGraph = { nodes, edges, graph, root: this.getNodeId(rootPackage, rootVersion), }; this.logger.debug(`Built graph with ${nodes.size} nodes and ${edges.length} edges`); return dependencyGraph; } private async processDependencies( parentName: string, parentVersion: string, dependencies: DependencyInfo[], manager: PackageManager, depth: number, nodes: Map<string, DependencyNode>, edges: DependencyEdge[], graph: graphlib.Graph ): Promise<void> { const parentId = this.getNodeId(parentName, parentVersion); for (const dep of dependencies) { const nodeId = this.getNodeId(dep.name, dep.version); // Create node if it doesn't exist if (!nodes.has(nodeId)) { const node: DependencyNode = { name: dep.name, version: dep.version, type: dep.type, manager: dep.manager || manager, depth, parent: parentId, metadata: { licenses: dep.licenses || [], vulnerabilities: dep.vulnerabilities || [], }, }; nodes.set(nodeId, node); graph.setNode(nodeId, node); } // Create edge const edge: DependencyEdge = { from: parentId, to: nodeId, type: depth === 1 ? 'direct' : 'transitive', }; edges.push(edge); graph.setEdge(parentId, nodeId, edge); // Process nested dependencies recursively if (dep.dependencies && dep.dependencies.length > 0) { await this.processDependencies( dep.name, dep.version, dep.dependencies, manager, depth + 1, nodes, edges, graph ); } } } private getNodeId(name: string, version: string): string { return `${name}@${version}`; } findCircularDependencies(graph: DependencyGraph): string[][] { this.logger.debug('Checking for circular dependencies'); const cycles: string[][] = []; const visited = new Set<string>(); const recursionStack = new Set<string>(); const dfs = (nodeId: string, path: string[]): void => { if (recursionStack.has(nodeId)) { // Found a cycle const cycleStart = path.indexOf(nodeId); cycles.push([...path.slice(cycleStart), nodeId]); return; } if (visited.has(nodeId)) { return; } visited.add(nodeId); recursionStack.add(nodeId); path.push(nodeId); const successors = graph.graph.successors(nodeId) || []; for (const successor of successors) { dfs(successor, [...path]); } recursionStack.delete(nodeId); }; // Start DFS from root dfs(graph.root, []); this.logger.debug(`Found ${cycles.length} circular dependencies`); return cycles; } findConflictingVersions(graph: DependencyGraph): Map<string, string[]> { this.logger.debug('Checking for version conflicts'); const conflicts = new Map<string, string[]>(); const packageVersions = new Map<string, Set<string>>(); // Collect all versions for each package for (const [_nodeId, node] of graph.nodes) { if (!packageVersions.has(node.name)) { packageVersions.set(node.name, new Set()); } packageVersions.get(node.name)!.add(node.version); } // Find packages with multiple versions for (const [packageName, versions] of packageVersions) { if (versions.size > 1) { conflicts.set(packageName, Array.from(versions)); } } this.logger.debug(`Found ${conflicts.size} packages with version conflicts`); return conflicts; } calculateDependencyMetrics(graph: DependencyGraph): { totalDependencies: number; directDependencies: number; transitiveDependencies: number; maxDepth: number; averageDepth: number; packageCount: number; } { let totalDependencies = 0; let directDependencies = 0; let transitiveDependencies = 0; let maxDepth = 0; let totalDepth = 0; const packageNames = new Set<string>(); for (const [_nodeId, node] of graph.nodes) { if (node.depth === 0) continue; // Skip root totalDependencies++; packageNames.add(node.name); if (node.depth === 1) { directDependencies++; } else { transitiveDependencies++; } maxDepth = Math.max(maxDepth, node.depth); totalDepth += node.depth; } return { totalDependencies, directDependencies, transitiveDependencies, maxDepth, averageDepth: totalDependencies > 0 ? totalDepth / totalDependencies : 0, packageCount: packageNames.size, }; } findCriticalPath(graph: DependencyGraph): string[] { // Find the longest path from root to any leaf (critical path) const longestPath: string[] = []; const visited = new Set<string>(); const dfs = (nodeId: string, path: string[]): void => { if (visited.has(nodeId)) return; visited.add(nodeId); path.push(nodeId); const successors = graph.graph.successors(nodeId) || []; if (successors.length === 0) { // Leaf node if (path.length > longestPath.length) { longestPath.splice(0, longestPath.length, ...path); } } else { for (const successor of successors) { dfs(successor, [...path]); } } path.pop(); }; dfs(graph.root, []); return longestPath; } generateVisualizationData(graph: DependencyGraph): { nodes: Array<{ id: string; name: string; version: string; type: string; depth: number; vulnerabilities: number; }>; edges: Array<{ from: string; to: string; type: string; }>; } { const nodes = Array.from(graph.nodes.values()).map(node => ({ id: this.getNodeId(node.name, node.version), name: node.name, version: node.version, type: node.type, depth: node.depth, vulnerabilities: node.metadata.vulnerabilities.length, })); const edges = graph.edges.map(edge => ({ from: edge.from, to: edge.to, type: edge.type, })); return { nodes, edges }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cbunting99/mcp-code-analysis-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server