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"
                )
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. While 'Generate a report' implies a read-only operation that creates output, it doesn't specify whether this tool scans files, requires specific permissions, has performance implications for large projects, or what format the report takes. The description lacks important behavioral context for a tool that presumably analyzes project structure.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence that gets straight to the point with no wasted words. It's appropriately sized for what it communicates, though what it communicates is minimal. The structure is clear and front-loaded with the core purpose.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given that there's an output schema (which should document the return format), the description doesn't need to explain return values. However, for a tool that analyzes project structure with 2 parameters and no annotations, the description is too minimal. It doesn't provide enough context about what 'project structure metrics' includes, how the tool works, or what the parameters control.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 0% schema description coverage for both parameters, the description provides no information about what 'output_filename' or 'ignore_patterns' mean or how they should be used. The description doesn't mention parameters at all, leaving the agent to guess their purpose from parameter names alone. This is inadequate for a tool with 2 parameters that have no schema documentation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description 'Generate a report of project structure metrics' clearly states the verb ('Generate') and resource ('report of project structure metrics'), making the purpose understandable. However, it doesn't distinguish this tool from its sibling 'code_collector' - both could potentially involve project analysis, so the distinction isn't explicit.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives. There's no mention of when this tool is appropriate, what prerequisites might be needed, or how it differs from the sibling 'code_collector' tool. The agent must infer usage context entirely from the tool name and description.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

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