Skip to main content
Glama
mdz-axo

PT-MCP (Paul Test Man Context Protocol)

by mdz-axo
analyze-dependencies.ts13.6 kB
/** * analyze_dependencies tool implementation * * Analyzes dependency graph with KG enrichment */ import { readFile, readdir } from 'fs/promises'; import { join, extname, relative, resolve, dirname } from 'path'; import { existsSync } from 'fs'; import { getDatabase } from '../knowledge-graph/database.js'; import { getYAGOResolver } from '../knowledge-graph/yago-resolver.js'; interface AnalyzeDependenciesArgs { path: string; include_external?: boolean; include_internal?: boolean; max_depth?: number; } interface Dependency { name: string; version?: string; type: 'external' | 'internal'; dependents: string[]; dependencies: string[]; depth: number; kg_enrichment?: { description?: string; yago_uri?: string; facts_count?: number; }; } interface DependencyGraph { nodes: Map<string, Dependency>; edges: Array<[string, string]>; circular: string[][]; orphans: string[]; stats: { total_external: number; total_internal: number; max_depth: number; circular_count: number; }; } export async function analyzeDependencies( args: AnalyzeDependenciesArgs ): Promise<{ content: Array<{ type: string; text: string }> }> { const { path, include_external = true, include_internal = true, max_depth = 5, } = args; const graph: DependencyGraph = { nodes: new Map(), edges: [], circular: [], orphans: [], stats: { total_external: 0, total_internal: 0, max_depth: 0, circular_count: 0, }, }; // Analyze external dependencies (from package.json) if (include_external) { await analyzeExternalDependencies(path, graph); } // Analyze internal dependencies (import/require statements) if (include_internal) { await analyzeInternalDependencies(path, graph, max_depth); } // Detect circular dependencies detectCircularDependencies(graph); // Enrich with knowledge graph await enrichDependenciesWithKG(graph); // Store in database as RDF triples await storeDependencyGraph(graph); // Format results const results = formatDependencyResults(graph); return { content: [ { type: 'text', text: results, }, ], }; } /** * Analyze external dependencies from package.json */ async function analyzeExternalDependencies( path: string, graph: DependencyGraph ): Promise<void> { const pkgPath = join(path, 'package.json'); if (!existsSync(pkgPath)) { return; } const pkgContent = await readFile(pkgPath, 'utf-8'); const pkg = JSON.parse(pkgContent); // Process dependencies const deps = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies, }; for (const [name, version] of Object.entries(deps)) { if (!graph.nodes.has(name)) { graph.nodes.set(name, { name, version: version as string, type: 'external', dependents: [], dependencies: [], depth: 1, }); graph.stats.total_external++; } } } /** * Analyze internal module dependencies */ async function analyzeInternalDependencies( path: string, graph: DependencyGraph, maxDepth: number ): Promise<void> { const files = await getCodeFiles(path); const moduleDeps = new Map<string, Set<string>>(); for (const file of files) { const content = await readFile(file, 'utf-8'); const imports = extractImports(content); const relPath = relative(path, file); if (!moduleDeps.has(relPath)) { moduleDeps.set(relPath, new Set()); } for (const imp of imports) { // Skip external dependencies if (!imp.startsWith('.') && !imp.startsWith('/')) { continue; } // Resolve relative import const resolved = resolveImport(file, imp, path); if (resolved) { moduleDeps.get(relPath)!.add(resolved); } } } // Build dependency graph for (const [module, deps] of moduleDeps.entries()) { if (!graph.nodes.has(module)) { graph.nodes.set(module, { name: module, type: 'internal', dependents: [], dependencies: [], depth: 0, }); graph.stats.total_internal++; } const node = graph.nodes.get(module)!; for (const dep of deps) { node.dependencies.push(dep); graph.edges.push([module, dep]); if (!graph.nodes.has(dep)) { graph.nodes.set(dep, { name: dep, type: 'internal', dependents: [], dependencies: [], depth: 0, }); graph.stats.total_internal++; } graph.nodes.get(dep)!.dependents.push(module); } } // Calculate depths calculateDepths(graph, maxDepth); } /** * Get all code files in directory */ async function getCodeFiles(dir: string): Promise<string[]> { const files: string[] = []; const entries = await readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = join(dir, entry.name); if (entry.name === 'node_modules' || entry.name === '.git' || entry.name.startsWith('.')) { continue; } if (entry.isDirectory()) { const subFiles = await getCodeFiles(fullPath); files.push(...subFiles); } else if (entry.isFile()) { const ext = extname(entry.name); if (['.ts', '.js', '.tsx', '.jsx'].includes(ext)) { files.push(fullPath); } } } return files; } /** * Extract import statements from code */ function extractImports(content: string): string[] { const imports: string[] = []; // ES6 imports: import ... from 'module' const es6Imports = content.matchAll(/import\s+(?:[\w*{},\s]+\s+from\s+)?['"]([^'"]+)['"]/g); for (const match of es6Imports) { imports.push(match[1]); } // CommonJS requires: require('module') const cjsRequires = content.matchAll(/require\(['"]([^'"]+)['"]\)/g); for (const match of cjsRequires) { imports.push(match[1]); } // Dynamic imports: import('module') const dynamicImports = content.matchAll(/import\(['"]([^'"]+)['"]\)/g); for (const match of dynamicImports) { imports.push(match[1]); } return imports; } /** * Resolve relative import path */ function resolveImport(fromFile: string, importPath: string, rootPath: string): string | null { try { const fromDir = dirname(fromFile); const resolved = resolve(fromDir, importPath); // Try common extensions const extensions = ['.ts', '.js', '.tsx', '.jsx', '.json']; for (const ext of extensions) { const withExt = resolved + ext; if (existsSync(withExt)) { return relative(rootPath, withExt); } } // Try index files for (const ext of extensions) { const indexFile = join(resolved, `index${ext}`); if (existsSync(indexFile)) { return relative(rootPath, indexFile); } } return null; } catch { return null; } } /** * Calculate dependency depths using BFS */ function calculateDepths(graph: DependencyGraph, maxDepth: number): void { // Find root nodes (no dependents) const roots: string[] = []; for (const [name, node] of graph.nodes.entries()) { if (node.type === 'internal' && node.dependents.length === 0) { roots.push(name); node.depth = 0; } } // BFS to calculate depths const queue = [...roots]; const visited = new Set<string>(); while (queue.length > 0) { const current = queue.shift()!; if (visited.has(current)) continue; visited.add(current); const node = graph.nodes.get(current)!; if (node.depth >= maxDepth) continue; for (const dep of node.dependencies) { const depNode = graph.nodes.get(dep); if (depNode && depNode.type === 'internal') { const newDepth = node.depth + 1; if (newDepth < depNode.depth || depNode.depth === 0) { depNode.depth = newDepth; graph.stats.max_depth = Math.max(graph.stats.max_depth, newDepth); } queue.push(dep); } } } // Find orphans (no dependencies and no dependents, internal only) for (const [name, node] of graph.nodes.entries()) { if ( node.type === 'internal' && node.dependencies.length === 0 && node.dependents.length === 0 ) { graph.orphans.push(name); } } } /** * Detect circular dependencies using DFS */ function detectCircularDependencies(graph: DependencyGraph): void { const visited = new Set<string>(); const recursionStack = new Set<string>(); const cycles: string[][] = []; function dfs(node: string, path: string[]): void { visited.add(node); recursionStack.add(node); path.push(node); const deps = graph.nodes.get(node)?.dependencies || []; for (const dep of deps) { if (!graph.nodes.has(dep)) continue; if (!visited.has(dep)) { dfs(dep, [...path]); } else if (recursionStack.has(dep)) { // Found cycle const cycleStart = path.indexOf(dep); const cycle = path.slice(cycleStart).concat(dep); cycles.push(cycle); } } recursionStack.delete(node); } for (const node of graph.nodes.keys()) { if (!visited.has(node)) { dfs(node, []); } } graph.circular = cycles; graph.stats.circular_count = cycles.length; } /** * Enrich dependencies with knowledge graph data */ async function enrichDependenciesWithKG(graph: DependencyGraph): Promise<void> { const yagoResolver = getYAGOResolver(); const externals = Array.from(graph.nodes.values()).filter((n) => n.type === 'external'); // Enrich top external dependencies for (const dep of externals.slice(0, 20)) { try { const entities = await yagoResolver.resolveEntity(dep.name, 1); if (entities.length > 0) { const entity = entities[0]; dep.kg_enrichment = { description: entity.description, yago_uri: entity.uri, facts_count: entity.facts.length, }; } } catch (error) { console.error(`Failed to enrich ${dep.name}:`, error); } } } /** * Store dependency graph as RDF triples */ async function storeDependencyGraph(graph: DependencyGraph): Promise<void> { const db = await getDatabase(); for (const [source, target] of graph.edges) { try { await db.insertRDFTriple({ subject: `module:${source}`, predicate: 'dependsOn', object: `module:${target}`, graph: 'dependencies', }); } catch (error) { // Ignore duplicate entries } } } /** * Format dependency results for display */ function formatDependencyResults(graph: DependencyGraph): string { const lines: string[] = []; lines.push('# Dependency Analysis'); lines.push(''); // Statistics lines.push('## Statistics'); lines.push(`- **Total External:** ${graph.stats.total_external}`); lines.push(`- **Total Internal:** ${graph.stats.total_internal}`); lines.push(`- **Max Depth:** ${graph.stats.max_depth}`); lines.push(`- **Circular Dependencies:** ${graph.stats.circular_count}`); lines.push(`- **Orphan Modules:** ${graph.orphans.length}`); lines.push(''); // External dependencies with KG enrichment if (graph.stats.total_external > 0) { lines.push('## External Dependencies'); const externals = Array.from(graph.nodes.values()) .filter((n) => n.type === 'external') .sort((a, b) => b.dependents.length - a.dependents.length); for (const dep of externals.slice(0, 20)) { lines.push(`### ${dep.name}`); lines.push(`- **Version:** ${dep.version || 'N/A'}`); if (dep.kg_enrichment) { if (dep.kg_enrichment.description) { lines.push(`- **Description:** ${dep.kg_enrichment.description}`); } if (dep.kg_enrichment.facts_count) { lines.push(`- **Knowledge Graph:** ${dep.kg_enrichment.facts_count} facts in YAGO`); } } lines.push(''); } } // Circular dependencies if (graph.circular.length > 0) { lines.push('## Circular Dependencies'); lines.push(''); lines.push('⚠️ **Warning:** Circular dependencies detected!'); lines.push(''); for (let i = 0; i < graph.circular.length; i++) { const cycle = graph.circular[i]; lines.push(`${i + 1}. ${cycle.join(' → ')}`); } lines.push(''); } // Orphan modules if (graph.orphans.length > 0) { lines.push('## Orphan Modules'); lines.push(''); lines.push('These modules have no dependencies or dependents:'); lines.push(''); for (const orphan of graph.orphans) { lines.push(`- ${orphan}`); } lines.push(''); } // Top dependencies by usage const byUsage = Array.from(graph.nodes.values()) .filter((n) => n.type === 'internal') .sort((a, b) => b.dependents.length - a.dependents.length) .slice(0, 10); if (byUsage.length > 0) { lines.push('## Most Depended Upon (Internal)'); lines.push(''); for (const dep of byUsage) { lines.push(`- **${dep.name}** (${dep.dependents.length} dependents)`); } lines.push(''); } return lines.join('\n'); }

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/mdz-axo/pt-mcp'

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