aef8791151
- Added Appshell component with responsive navbar and main content area - Integrated ColorSchemeToggle for light/dark mode switching - Created Welcome component with styled title and introductory text - Developed ChatPage for LLM interaction with WebSocket support - Implemented Biblioteca for managing notes with rich text editor - Added LoginPage for user authentication with error handling - Introduced MessageList and MessageBubble components for chat messages - Styled components with CSS modules for consistent design
134 lines
5.1 KiB
Python
134 lines
5.1 KiB
Python
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")
|
|
|