MCP Source Tree Server

by owayo
Verified
import asyncio import json import os import sys from typing import Any, Dict, Optional import pathspec from mcp.server.fastmcp import FastMCP # Initialize FastMCP server mcp = FastMCP("src-tree") def read_gitignore(base_path: str) -> Optional[pathspec.PathSpec]: """ Read .gitignore file and return a PathSpec object Args: base_path: Base directory path containing .gitignore Returns: PathSpec object, or None if .gitignore doesn't exist """ gitignore_path = os.path.join(base_path, ".gitignore") if not os.path.exists(gitignore_path): return None with open(gitignore_path, "r") as f: # Remove empty lines and comments lines = [ line.strip() for line in f.readlines() if line.strip() and not line.startswith("#") ] gitignore = pathspec.PathSpec.from_lines( pathspec.patterns.GitWildMatchPattern, lines ) return gitignore def should_ignore( path: str, base_path: str, gitignore: Optional[pathspec.PathSpec] = None ) -> bool: """ Determine if the specified path should be ignored Args: path: Path to check base_path: Base directory path containing .gitignore gitignore: PathSpec object from .gitignore Returns: True if path should be ignored """ # Ignore directories starting with . if os.path.isdir(path) and os.path.basename(path).startswith("."): return True # Check if path matches .gitignore rules if gitignore: relative_path = os.path.relpath(path, base_path) # Add trailing slash for directories if os.path.isdir(path): relative_path = relative_path + os.sep # Normalize path (unify slashes) relative_path = relative_path.replace(os.sep, "/") if gitignore.match_file(relative_path): return True return False def build_tree( path: str, base_path: str, gitignore: Optional[pathspec.PathSpec] = None, is_root: bool = True, ) -> Dict[str, Any]: """ Build directory tree structure Args: path: Directory path to traverse base_path: Base directory path containing .gitignore gitignore: PathSpec object from .gitignore is_root: Whether this is the root directory Returns: Dictionary representing directory tree """ # Check if path should be ignored if should_ignore(path, base_path, gitignore): return None name = os.path.abspath(path) if is_root else os.path.basename(path) if os.path.isfile(path): return {"name": name, "type": "file"} result = {"name": name, "type": "directory", "children": []} try: items = os.listdir(path) for item in sorted(items): item_path = os.path.join(path, item) child = build_tree(item_path, base_path, gitignore, is_root=False) if child: result["children"].append(child) except PermissionError: pass return result @mcp.prompt() async def src_tree(directory: str) -> str: """ Get file tree as JSON string Returns: JSON string representing the file tree """ if not os.path.exists(directory): return json.dumps({"error": "directory not found"}, indent=2) gitignore = read_gitignore(directory) tree = build_tree(directory, directory, gitignore) return json.dumps(tree, indent=2, ensure_ascii=False) @mcp.tool() async def get_src_tree(directory: str) -> str: """ Generate a file tree for the specified directory, filtering files based on .gitignore. Traverses the filesystem and generates a JSON-formatted tree structure that preserves hierarchy. """ if not os.path.exists(directory): return json.dumps({"error": "directory not found"}, indent=2) gitignore = read_gitignore(directory) tree = build_tree(directory, directory, gitignore) return json.dumps(tree, indent=2, ensure_ascii=False) if __name__ == "__main__": args = sys.argv[1:] if not args: mcp.run(transport="stdio") elif args[0] == "test" and len(args) == 2: # Create event loop to run async function result = asyncio.run(get_src_tree(args[1])) print(result)