from fastmcp import FastMCP from pathlib import Path import shutil from datetime import datetime # Directorio base seguro SANDBOX_DIR = Path("./sandbox").resolve() SANDBOX_DIR.mkdir(parents=True, exist_ok=True) def safe_path(requested_path: str) -> Path: """Siempre interpreta la ruta como relativa al SANDBOX_DIR, incluso si empieza con '/'.""" # Normaliza la ruta quitando el primer '/' normalized = requested_path.strip().lstrip("/") full_path = (SANDBOX_DIR / normalized).resolve() if not full_path.is_relative_to(SANDBOX_DIR): raise ValueError("Ruta fuera del directorio permitido.") return full_path mcp = FastMCP() @mcp.tool(description="Lee y devuelve el contenido de un archivo de texto ubicado en el sistema de archivos seguro. El archivo debe estar dentro del sandbox.") def read_file(path: str) -> str: try: file_path = safe_path(path) if not file_path.is_file(): raise FileNotFoundError(f"Archivo '{path}' no encontrado.") return file_path.read_text(encoding="utf-8") except Exception as e: return f"⚠️ Error al leer archivo '{path}': {str(e)}" @mcp.tool(description="Escribe contenido de texto en un archivo dentro del sandbox. Si el archivo ya existe, será sobrescrito.") def write_file(path: str, content: str) -> str: file_path = safe_path(path) file_path.parent.mkdir(parents=True, exist_ok=True) file_path.write_text(content, encoding="utf-8") return "Archivo guardado correctamente." @mcp.tool(description="Elimina de forma segura un archivo ubicado dentro del sandbox.") def delete_file(path: str) -> str: file_path = safe_path(path) if not file_path.is_file(): raise FileNotFoundError("Archivo no encontrado.") file_path.unlink() return "Archivo eliminado." @mcp.tool(description="Crea una carpeta (y sus carpetas padre si es necesario) dentro del sandbox.") def create_folder(path: str) -> str: folder_path = safe_path(path) folder_path.mkdir(parents=True, exist_ok=True) return "Carpeta creada." @mcp.tool(description="Lista archivos y carpetas dentro de una ruta del sandbox.") def list_directory(path: str = ".") -> list[str]: folder = safe_path(path) if not folder.is_dir(): raise NotADirectoryError("Ruta no corresponde a una carpeta.") return sorted(str(p.relative_to(SANDBOX_DIR)) for p in folder.iterdir()) @mcp.tool(description="Muestra la estructura de carpetas y archivos como un árbol, desde una ruta dentro del sandbox.") def tree(path: str = ".", depth: int = 3) -> str: base = safe_path(path) if not base.is_dir(): raise NotADirectoryError("Ruta no corresponde a una carpeta.") tree_output = [] def walk(dir_path: Path, prefix: str = "", level: int = 0): if level > depth: return entries = sorted(dir_path.iterdir()) for i, entry in enumerate(entries): connector = "└── " if i == len(entries) - 1 else "├── " tree_output.append(f"{prefix}{connector}{entry.name}") if entry.is_dir(): extension = " " if i == len(entries) - 1 else "│ " walk(entry, prefix + extension, level + 1) tree_output.append(f"{base.name}/") walk(base) return "\n".join(tree_output) @mcp.tool(description="Devuelve información detallada sobre un archivo: tamaño en bytes, fecha de modificación y tipo.") def file_info(path: str) -> dict: fpath = safe_path(path) if not fpath.exists(): raise FileNotFoundError("Archivo no encontrado.") return { "nombre": fpath.name, "tipo": "carpeta" if fpath.is_dir() else "archivo", "tamaño_bytes": fpath.stat().st_size, "última_modificación": datetime.fromtimestamp(fpath.stat().st_mtime).isoformat(), "relativo": str(fpath.relative_to(SANDBOX_DIR)) } @mcp.tool(description="Copia un archivo o carpeta dentro del sandbox a otra ruta.") def copy_file(src: str, dest: str) -> str: src_path = safe_path(src) dest_path = safe_path(dest) if src_path.is_dir(): shutil.copytree(src_path, dest_path, dirs_exist_ok=True) else: dest_path.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(src_path, dest_path) return "Copia completada." @mcp.tool(description="Mueve o renombra un archivo o carpeta dentro del sandbox.") def move_file(src: str, dest: str) -> str: src_path = safe_path(src) dest_path = safe_path(dest) dest_path.parent.mkdir(parents=True, exist_ok=True) src_path.rename(dest_path) return "Movimiento completado." @mcp.tool(description="Elimina todos los archivos y subcarpetas dentro de una carpeta del sandbox.") def clear_folder(path: str) -> str: folder_path = safe_path(path) if not folder_path.is_dir(): raise NotADirectoryError("La ruta no es una carpeta.") for item in folder_path.iterdir(): if item.is_file() or item.is_symlink(): item.unlink() elif item.is_dir(): shutil.rmtree(item) return "Carpeta vaciada." if __name__ == "__main__": mcp.run(transport="streamable-http", host="127.0.0.1", port=4201, path="/fs")