Skip to main content
Glama

Smart Code Search MCP Server

graph_export.py13.6 kB
""" Lightweight Graph Export Tool for SCS-MCP Generates dependency graphs in text-based formats without external dependencies """ import json from pathlib import Path from typing import Dict, List, Set, Tuple, Optional, Any from collections import defaultdict class GraphExporter: """Export dependency graphs in various text formats""" def __init__(self, dependency_analyzer=None, coupling_analyzer=None): """ Initialize graph exporter Args: dependency_analyzer: DependencyAnalyzer instance coupling_analyzer: CouplingAnalyzer instance """ self.dependency_analyzer = dependency_analyzer self.coupling_analyzer = coupling_analyzer def export_to_dot(self, graph_type: str = "imports", title: str = "Dependency Graph") -> str: """ Export graph to DOT format (Graphviz) Args: graph_type: Type of graph ("imports", "calls", "inheritance") title: Graph title Returns: DOT format string """ dot_lines = [ f'digraph "{title}" {{', ' rankdir=LR;', ' node [shape=box, style=rounded];', '' ] # Get the appropriate graph data if graph_type == "imports" and self.dependency_analyzer: graph = self.dependency_analyzer.import_graph dot_lines.append(' // Import dependencies') for source, targets in graph.items(): source_name = self._clean_node_name(source) for target in targets: target_name = self._clean_node_name(target) dot_lines.append(f' "{source_name}" -> "{target_name}";') elif graph_type == "calls" and self.dependency_analyzer: graph = self.dependency_analyzer.call_graph dot_lines.append(' // Function call graph') for caller, callees in graph.items(): caller_name = self._clean_node_name(caller) for callee in callees: callee_name = self._clean_node_name(callee) dot_lines.append(f' "{caller_name}" -> "{callee_name}";') elif graph_type == "inheritance" and self.dependency_analyzer: graph = self.dependency_analyzer.inheritance_tree dot_lines.append(' // Class inheritance') for child, parents in graph.items(): child_name = self._clean_node_name(child) for parent in parents: parent_name = self._clean_node_name(parent) dot_lines.append(f' "{child_name}" -> "{parent_name}" [label="inherits"];') dot_lines.append('}') return '\n'.join(dot_lines) def export_to_mermaid(self, graph_type: str = "imports", title: str = "Dependency Graph") -> str: """ Export graph to Mermaid diagram format (renders in GitHub/GitLab markdown) Args: graph_type: Type of graph ("imports", "calls", "inheritance") title: Graph title Returns: Mermaid format string """ mermaid_lines = [ '```mermaid', 'graph TD', f' %% {title}' ] # Track nodes to avoid duplicates nodes_seen = set() # Get the appropriate graph data if graph_type == "imports" and self.dependency_analyzer: graph = self.dependency_analyzer.import_graph mermaid_lines.append(' %% Import dependencies') for source, targets in graph.items(): source_id = self._to_mermaid_id(source) source_label = self._clean_node_name(source) if source_id not in nodes_seen: mermaid_lines.append(f' {source_id}["{source_label}"]') nodes_seen.add(source_id) for target in targets: target_id = self._to_mermaid_id(target) target_label = self._clean_node_name(target) if target_id not in nodes_seen: mermaid_lines.append(f' {target_id}["{target_label}"]') nodes_seen.add(target_id) mermaid_lines.append(f' {source_id} --> {target_id}') elif graph_type == "calls" and self.dependency_analyzer: graph = self.dependency_analyzer.call_graph mermaid_lines.append(' %% Function call graph') for caller, callees in graph.items(): caller_id = self._to_mermaid_id(caller) caller_label = self._clean_node_name(caller) if caller_id not in nodes_seen: mermaid_lines.append(f' {caller_id}["{caller_label}"]') nodes_seen.add(caller_id) for callee in callees: callee_id = self._to_mermaid_id(callee) callee_label = self._clean_node_name(callee) if callee_id not in nodes_seen: mermaid_lines.append(f' {callee_id}["{callee_label}"]') nodes_seen.add(callee_id) mermaid_lines.append(f' {caller_id} --> {callee_id}') elif graph_type == "inheritance" and self.dependency_analyzer: graph = self.dependency_analyzer.inheritance_tree mermaid_lines.append(' %% Class inheritance') for child, parents in graph.items(): child_id = self._to_mermaid_id(child) child_label = self._clean_node_name(child) if child_id not in nodes_seen: mermaid_lines.append(f' {child_id}["{child_label}"]') nodes_seen.add(child_id) for parent in parents: parent_id = self._to_mermaid_id(parent) parent_label = self._clean_node_name(parent) if parent_id not in nodes_seen: mermaid_lines.append(f' {parent_id}["{parent_label}"]') nodes_seen.add(parent_id) mermaid_lines.append(f' {child_id} -.->|inherits| {parent_id}') mermaid_lines.append('```') return '\n'.join(mermaid_lines) def export_to_json(self, graph_type: str = "imports", include_metadata: bool = True) -> str: """ Export graph to JSON format for custom visualization Args: graph_type: Type of graph ("imports", "calls", "inheritance") include_metadata: Include additional metadata Returns: JSON string """ graph_data = { "type": graph_type, "nodes": [], "edges": [], "metadata": {} } nodes_map = {} # Track node IDs node_id = 0 # Get the appropriate graph data graph = None if graph_type == "imports" and self.dependency_analyzer: graph = self.dependency_analyzer.import_graph elif graph_type == "calls" and self.dependency_analyzer: graph = self.dependency_analyzer.call_graph elif graph_type == "inheritance" and self.dependency_analyzer: graph = self.dependency_analyzer.inheritance_tree if graph: for source, targets in graph.items(): # Add source node if source not in nodes_map: nodes_map[source] = node_id graph_data["nodes"].append({ "id": node_id, "label": self._clean_node_name(source), "type": self._get_node_type(source) }) node_id += 1 # Add target nodes and edges for target in targets: if target not in nodes_map: nodes_map[target] = node_id graph_data["nodes"].append({ "id": node_id, "label": self._clean_node_name(target), "type": self._get_node_type(target) }) node_id += 1 graph_data["edges"].append({ "source": nodes_map[source], "target": nodes_map[target], "type": graph_type }) if include_metadata: graph_data["metadata"] = { "total_nodes": len(graph_data["nodes"]), "total_edges": len(graph_data["edges"]), "graph_type": graph_type } # Calculate basic metrics if graph_data["edges"]: in_degree = defaultdict(int) out_degree = defaultdict(int) for edge in graph_data["edges"]: out_degree[edge["source"]] += 1 in_degree[edge["target"]] += 1 graph_data["metadata"]["max_out_degree"] = max(out_degree.values()) if out_degree else 0 graph_data["metadata"]["max_in_degree"] = max(in_degree.values()) if in_degree else 0 return json.dumps(graph_data, indent=2) def detect_circular_dependencies(self, graph_type: str = "imports") -> List[List[str]]: """ Detect circular dependencies in the graph Args: graph_type: Type of graph to analyze Returns: List of circular dependency paths """ graph = None if graph_type == "imports" and self.dependency_analyzer: graph = self.dependency_analyzer.import_graph elif graph_type == "calls" and self.dependency_analyzer: graph = self.dependency_analyzer.call_graph if not graph: return [] cycles = [] visited = set() rec_stack = set() def _dfs(node, path): visited.add(node) rec_stack.add(node) path.append(node) if node in graph: for neighbor in graph[node]: if neighbor not in visited: _dfs(neighbor, path[:]) elif neighbor in rec_stack: # Found a cycle cycle_start = path.index(neighbor) cycle = path[cycle_start:] + [neighbor] cycles.append(cycle) rec_stack.remove(node) for node in graph: if node not in visited: _dfs(node, []) return cycles def _clean_node_name(self, name: str) -> str: """Clean node name for display""" # Remove long paths, keep just filename or last component if '/' in name or '\\' in name: return Path(name).name return name def _to_mermaid_id(self, name: str) -> str: """Convert name to valid Mermaid node ID""" # Replace special characters with underscores import re return 'node_' + re.sub(r'[^a-zA-Z0-9_]', '_', name) def _get_node_type(self, name: str) -> str: """Determine node type from name""" if name.endswith('.py'): return 'file' elif '.' in name: return 'module' elif name[0].isupper(): return 'class' else: return 'function' def generate_dependency_graph(analyzer_data: Dict[str, Any], output_format: str = "mermaid", graph_type: str = "imports") -> str: """ MCP tool function to generate dependency graphs Args: analyzer_data: Data from dependency or coupling analyzer output_format: Output format ("dot", "mermaid", "json") graph_type: Type of graph ("imports", "calls", "inheritance") Returns: Graph in requested format """ # Create mock analyzer with the provided data from src.core.dependency_analyzer import DependencyAnalyzer analyzer = DependencyAnalyzer(Path.cwd()) # Populate analyzer with provided data if "import_graph" in analyzer_data: analyzer.import_graph = analyzer_data["import_graph"] if "call_graph" in analyzer_data: analyzer.call_graph = analyzer_data["call_graph"] if "inheritance_tree" in analyzer_data: analyzer.inheritance_tree = analyzer_data["inheritance_tree"] exporter = GraphExporter(dependency_analyzer=analyzer) if output_format == "dot": return exporter.export_to_dot(graph_type) elif output_format == "mermaid": return exporter.export_to_mermaid(graph_type) elif output_format == "json": return exporter.export_to_json(graph_type) else: raise ValueError(f"Unsupported format: {output_format}")

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/stevenjjobson/scs-mcp'

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