Skip to main content
Glama
aindreyway

MCP Server Neurolorap

by aindreyway

project_structure_reporter

Analyze and document project structure metrics to generate comprehensive reports for codebase organization and review.

Instructions

Generate a report of project structure metrics

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
output_filenameNoPROJECT_STRUCTURE_REPORT.md
ignore_patternsNo

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
resultYes

Implementation Reference

  • The main handler function for the 'project_structure_reporter' tool. It creates a ProjectStructureReporter instance, analyzes the project, generates a markdown report, and returns the output path.
    async def project_structure_reporter(
        output_filename: str = "PROJECT_STRUCTURE_REPORT.md",
        ignore_patterns: list[str] | None = None,
    ) -> str:
        """Generate a report of project structure metrics."""
        logger.debug("Tool call: project_structure_reporter")
        logger.debug(
            "Arguments: output_filename=%s, ignore_patterns=%s",
            output_filename,
            ignore_patterns,
        )
    
        try:
            root_path = get_project_root()
            reporter = ProjectStructureReporter(
                root_dir=root_path,
                ignore_patterns=ignore_patterns,
            )
    
            logger.info("Starting project structure analysis")
            report_data = reporter.analyze_project_structure()
    
            output_path = root_path / ".neurolora" / output_filename
            output_path.parent.mkdir(parents=True, exist_ok=True)
    
            reporter.generate_markdown_report(report_data, output_path)
    
            return f"Project structure report generated: {output_path}"
    
        except Exception as e:
            error_msg = f"Unexpected error generating report: {e}"
            logger.error(error_msg, exc_info=True)
            return f"Error generating report: {str(e)}"
    
    # Code collector tool
  • Registration of the 'project_structure_reporter' tool using the MCP server's mcp.tool decorator.
    mcp.tool(
        name="project_structure_reporter",
        description="Generate a report of project structure metrics",
    )(project_structure_reporter)
  • TypedDict definitions for FileData and ReportData, providing type schemas for the analysis results used by the tool.
    class FileData(TypedDict):
        """Type definition for file analysis data."""
    
        path: str
        size_bytes: int
        tokens: int
        lines: int
        is_large: bool
        is_complex: bool
        error: bool
    
    
    class ReportData(TypedDict):
        """Type definition for project analysis report."""
    
        last_updated: str
        files: List[FileData]
        total_size: int
        total_lines: int
        total_tokens: int
        large_files: int
        error_files: int
  • The ProjectStructureReporter class containing all the core logic for analyzing project structure, counting lines/tokens, ignoring patterns, and generating the markdown report. Used by the handler.
    class ProjectStructureReporter:
        """Analyzes project structure and generates reports on file metrics."""
    
        def __init__(
            self, root_dir: Path, ignore_patterns: Optional[List[str]] = None
        ):
            """Initialize reporter with root directory and ignore patterns.
    
            Args:
                root_dir: Root directory to analyze
                ignore_patterns: List of patterns to ignore (glob format)
            """
            self.root_dir = root_dir
            self.large_file_threshold = 1024 * 1024  # 1MB
            self.large_lines_threshold = 300
    
            # Load ignore patterns from .neuroloraignore and combine with provided
            # patterns
            self.ignore_patterns = self.load_ignore_patterns()
            if ignore_patterns:
                self.ignore_patterns.extend(ignore_patterns)
    
        def load_ignore_patterns(self) -> List[str]:
            """Load ignore patterns from .neuroloraignore file.
    
            Returns:
                List[str]: List of ignore patterns
            """
            # Check for .neuroloraignore file
            ignore_file = self.root_dir / ".neuroloraignore"
            patterns: List[str] = []
    
            try:
                if ignore_file.exists():
                    with open(ignore_file, "r", encoding="utf-8") as f:
                        for line in f:
                            line = line.strip()
                            # Skip empty lines and comments
                            if line and not line.startswith("#"):
                                patterns.append(line)
            except (
                FileNotFoundError,
                PermissionError,
                UnicodeDecodeError,
                IOError,
            ):
                pass
    
            return patterns
    
        def should_ignore(self, path: Path) -> bool:
            """Check if file/directory should be ignored based on patterns.
    
            Args:
                path: Path to check
    
            Returns:
                bool: True if path should be ignored
            """
            try:
                relative_path = path.relative_to(self.root_dir)
                str_path = str(relative_path)
    
                # Check each ignore pattern
                for pattern in self.ignore_patterns:
                    # Handle directory patterns (ending with /)
                    if pattern.endswith("/"):
                        if any(
                            part == pattern[:-1] for part in relative_path.parts
                        ):
                            return True
                    # Handle file patterns
                    elif fnmatch.fnmatch(str_path, pattern) or fnmatch.fnmatch(
                        path.name, pattern
                    ):
                        return True
    
                # Additional checks
                if "FULL_CODE_" in str(path):
                    return True
    
                # Always ignore .neuroloraignore files
                if path.name == ".neuroloraignore":
                    return True
    
                try:
                    if (
                        path.exists() and path.stat().st_size > 1024 * 1024
                    ):  # Skip files > 1MB
                        return True
                except (FileNotFoundError, PermissionError):
                    return True
    
                return False
            except ValueError:
                return True
    
        def count_lines(self, filepath: Path) -> int:
            """Count non-empty lines in file.
    
            Args:
                filepath: Path to file
    
            Returns:
                int: Number of non-empty lines
            """
            try:
                # Try to detect if file is binary
                with open(filepath, "rb") as f:
                    chunk = f.read(1024)
                    if b"\0" in chunk:  # Binary file detection
                        return 0
    
                # If not binary, count lines
                with filepath.open("r", encoding="utf-8") as f:
                    return sum(1 for line in f if line.strip())
            except (UnicodeDecodeError, OSError):
                return 0
    
        def estimate_tokens(self, size_bytes: int) -> int:
            """Estimate number of tokens based on file size.
    
            Args:
                size_bytes: File size in bytes
    
            Returns:
                int: Estimated number of tokens (4 chars ≈ 1 token)
            """
            return size_bytes // 4
    
        def analyze_file(self, filepath: Path) -> FileData:
            """Analyze single file metrics.
    
            Args:
                filepath: Path to file
    
            Returns:
                dict: File metrics including size, lines, and tokens
            """
            try:
                size_bytes = filepath.stat().st_size
    
                # Skip detailed analysis for large files
                if size_bytes > self.large_file_threshold:
                    return {
                        "path": str(filepath.relative_to(self.root_dir)),
                        "size_bytes": size_bytes,
                        "tokens": 0,
                        "lines": 0,
                        "is_large": True,
                        "is_complex": False,
                        "error": False,
                    }
    
                lines = self.count_lines(filepath)
                tokens = self.estimate_tokens(size_bytes)
    
                return {
                    "path": str(filepath.relative_to(self.root_dir)),
                    "size_bytes": size_bytes,
                    "tokens": tokens,
                    "lines": lines,
                    "is_large": False,
                    "is_complex": lines > self.large_lines_threshold,
                    "error": False,
                }
            except OSError:
                # Handle file access errors gracefully
                return {
                    "path": str(filepath.relative_to(self.root_dir)),
                    "size_bytes": 0,
                    "tokens": 0,
                    "lines": 0,
                    "is_large": False,
                    "is_complex": False,
                    "error": True,
                }
    
        def analyze_project_structure(self) -> ReportData:
            """Analyze entire project structure.
    
            Returns:
                dict: Project metrics including all files and totals
            """
            report_data: ReportData = {
                "last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                "files": [],
                "total_size": 0,
                "total_lines": 0,
                "total_tokens": 0,
                "large_files": 0,
                "error_files": 0,
            }
    
            for dirpath, dirs, files in os.walk(self.root_dir):
                current_path = Path(dirpath)
    
                # Skip ignored directories
                dirs[:] = [
                    d for d in dirs if not self.should_ignore(current_path / d)
                ]
    
                for filename in files:
                    filepath = current_path / filename
                    if self.should_ignore(filepath):
                        continue
    
                    file_data = self.analyze_file(filepath)
                    report_data["files"].append(file_data)
    
                    report_data["total_size"] += file_data["size_bytes"]
                    if file_data.get("error", False):
                        report_data["error_files"] += 1
                    elif file_data["is_large"]:
                        report_data["large_files"] += 1
                    else:
                        report_data["total_lines"] += file_data["lines"]
                        report_data["total_tokens"] += file_data["tokens"]
    
            return report_data
    
        def generate_markdown_report(
            self, report_data: ReportData, output_path: Path
        ) -> None:
            """Generate markdown report from analysis data.
    
            Args:
                report_data: Analysis results
                output_path: Where to save the report
            """
            with output_path.open("w", encoding="utf-8") as f:
                # Header
                f.write("# Project Structure Report\n\n")
                f.write(
                    "Description: Project structure analysis with metrics "
                    "and recommendations\n"
                )
                f.write(f"Generated: {report_data['last_updated']}\n\n")
    
                # Files section with tree structure
                f.write("## Project Tree\n\n")
                files = sorted(report_data["files"], key=lambda x: x["path"])
    
                # Build tree structure
                current_path: List[str] = []
                for file_data in files:
                    parts = file_data["path"].split("/")
    
                    # Find common prefix
                    i = 0
                    while i < len(current_path) and i < len(parts) - 1:
                        if current_path[i] != parts[i]:
                            break
                        i += 1
    
                    # Remove different parts
                    while len(current_path) > i:
                        current_path.pop()
    
                    # Add new parts
                    while i < len(parts) - 1:
                        f.write("│   " * len(current_path))
                        f.write("├── " + parts[i] + "/\n")
                        current_path.append(parts[i])
                        i += 1
    
                    # Write file entry
                    f.write("│   " * len(current_path))
                    f.write("├── ")
                    self._write_file_entry(f, file_data, tree_format=True)
    
                # Summary
                f.write("\n## Summary\n\n")
                total_kb = report_data["total_size"] / 1024
                f.write("| Metric | Value |\n")
                f.write("|--------|-------|\n")
                f.write(f"| Total Size | {total_kb:.1f}KB |\n")
                f.write(f"| Total Lines | {report_data['total_lines']} |\n")
                f.write(f"| Total Tokens | ~{report_data['total_tokens']} |\n")
                f.write(f"| Large Files | {report_data['large_files']} |\n")
                if report_data["error_files"] > 0:
                    f.write(
                        f"| Files with Errors | {report_data['error_files']} |\n"
                    )
    
                # Notes
                f.write("\n## Notes\n\n")
                f.write("- 📦 File size indicators:\n")
                f.write("  - Files larger than 1MB are marked as large files\n")
                f.write(
                    "  - Size is shown in KB for files ≥ 1KB, " "bytes otherwise\n"
                )
                f.write("- 📊 Code metrics:\n")
                f.write("  - 🔴 indicates files with more than 300 lines\n")
                f.write("  - Token count is estimated (4 chars ≈ 1 token)\n")
                f.write("  - Empty lines are excluded from line count\n")
                f.write("- ⚠️ Processing:\n")
                f.write(
                    "  - Binary files and files with encoding errors "
                    "are skipped\n"
                )
                f.write("  - Files matching ignore patterns are excluded\n\n")
    
                # Recommendations
                f.write("## Recommendations\n\n")
                f.write(
                    "The following files might benefit from being split "
                    "into smaller modules:\n\n"
                )
                complex_files = [f for f in files if f["is_complex"]]
                if complex_files:
                    for file_data in sorted(
                        complex_files, key=lambda x: x["lines"], reverse=True
                    ):
                        lines = file_data["lines"]
                        suggested_modules = self._calculate_suggested_modules(
                            lines
                        )
                        avg_lines = lines // suggested_modules
                        f.write(f"- {file_data['path']} ({lines} lines) 🔴\n")
                        f.write(
                            f"  - Consider splitting into {suggested_modules} "
                            f"modules of ~{avg_lines} lines each\n"
                        )
                else:
                    f.write(
                        "No files currently exceed the recommended "
                        "size limit (300 lines).\n"
                    )
    
        def _calculate_suggested_modules(self, lines: int) -> int:
            """Calculate suggested number of modules for splitting a file.
    
            Args:
                lines: Number of lines in the file
    
            Returns:
                int: Suggested number of modules
            """
            return (
                lines + self.large_lines_threshold - 1
            ) // self.large_lines_threshold
    
        def _write_file_entry(
            self, f: Any, file_data: FileData, tree_format: bool = False
        ) -> None:
            """Write a single file entry in the report.
    
            Args:
                f: File handle to write to
                file_data: Data for the file entry
            """
            size_kb = file_data["size_bytes"] / 1024
            size_str = (
                f"{size_kb:.1f}KB"
                if size_kb >= 1
                else f"{file_data['size_bytes']}B"
            )
    
            filename = file_data["path"].split("/")[-1]
            if file_data.get("error", False):
                f.write(f"{filename} (⚠️ Error accessing file)\n")
            elif file_data["is_large"]:
                f.write(f"{filename} ({size_str}) ⚠️ Large file\n")
            else:
                complexity_marker = "🔴" if file_data["is_complex"] else ""
                f.write(
                    f"{filename} ({size_str}, ~{file_data['tokens']} tokens, "
                    f"{file_data['lines']} lines) {complexity_marker}\n"
                )

Tool Definition Quality

Score is being calculated. Check back soon.

Install Server

Other Tools

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/aindreyway/mcp-server-neurolora-p'

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