feat: Implement main application shell with navigation and color scheme toggle

- 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
This commit is contained in:
2025-06-21 02:01:21 +02:00
parent 3d5deef0fb
commit aef8791151
101 changed files with 169 additions and 166 deletions
@@ -0,0 +1,133 @@
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")
@@ -0,0 +1,92 @@
from fastmcp import FastMCP
mcp = FastMCP()
@mcp.tool(description="Suma dos números enteros.")
def add(a: int, b: int) -> int:
return a + b
@mcp.tool(description="Resta dos números enteros.")
def subtract(a: int, b: int) -> int:
return a - b
@mcp.tool(description="Multiplica dos números enteros.")
def multiply(a: int, b: int) -> int:
return a * b
@mcp.tool(description="Divide dos números y devuelve el resultado flotante.")
def divide(a: float, b: float) -> float:
if b == 0:
raise ValueError("No se puede dividir entre cero.")
return a / b
@mcp.tool(description="Calcula el módulo de dos números enteros.")
def modulo(a: int, b: int) -> int:
return a % b
@mcp.tool(description="Concatena dos cadenas de texto.")
def concat(a: str, b: str) -> str:
return a + b
@mcp.tool(description="Devuelve la longitud de una cadena.")
def string_length(s: str) -> int:
return len(s)
@mcp.tool(description="Convierte una cadena a mayúsculas.")
def to_upper(s: str) -> str:
return s.upper()
@mcp.tool(description="Convierte una cadena a minúsculas.")
def to_lower(s: str) -> str:
return s.lower()
@mcp.tool(description="Devuelve la suma de todos los elementos en una lista de enteros.")
def sum_list(numbers: list[int]) -> int:
return sum(numbers)
@mcp.tool(description="Devuelve el valor máximo en una lista de enteros.")
def max_in_list(numbers: list[int]) -> int:
return max(numbers)
@mcp.tool(description="Verifica si un número es par.")
def is_even(n: int) -> bool:
return n % 2 == 0
@mcp.tool(description="Verifica si una cadena es un palíndromo.")
def is_palindrome(s: str) -> bool:
return s == s[::-1]
@mcp.tool(description="Calcula el factorial de un número entero positivo.")
def factorial(n: int) -> int:
if n < 0:
raise ValueError("El factorial no está definido para negativos.")
if n == 0:
return 1
result = 1
for i in range(1, n + 1):
result *= i
return result
@mcp.tool(description="Devuelve los primeros n números de Fibonacci.")
def fibonacci(n: int) -> list[int]:
if n <= 0:
return []
seq = [0, 1]
while len(seq) < n:
seq.append(seq[-1] + seq[-2])
return seq[:n]
@mcp.tool(description="Devuelve si un número es primo.")
def is_prime(n: int) -> bool:
if n <= 1:
return False
for i in range(2, int(n**0.5) + 1):
if n % i == 0:
return False
return True
if __name__ == "__main__":
mcp.run(transport="streamable-http", host="127.0.0.1", port=4200, path="/math")
# mcp.run(transport="stdio")
@@ -0,0 +1,69 @@
from fastmcp import FastMCP
import uuid
import datetime
import socket
import platform
import os
mcp = FastMCP()
@mcp.tool(description="Genera un UUID versión 4.")
def generate_uuid() -> str:
return str(uuid.uuid4())
@mcp.tool(description="Devuelve la fecha y hora actuales en formato ISO 8601.")
def current_datetime() -> str:
return datetime.datetime.now().isoformat()
@mcp.tool(description="Devuelve solo la fecha actual.")
def current_date() -> str:
return datetime.date.today().isoformat()
@mcp.tool(description="Devuelve el nombre del host actual.")
def get_hostname() -> str:
return socket.gethostname()
@mcp.tool(description="Devuelve el sistema operativo actual.")
def get_os() -> str:
return platform.system()
@mcp.tool(description="Devuelve el nombre del usuario actual del sistema.")
def get_current_user() -> str:
return os.getlogin()
@mcp.tool(description="Invierte un valor booleano.")
def invert_boolean(flag: bool) -> bool:
return not flag
# @mcp.tool(description="Devuelve los archivos y carpetas del directorio actual.")
# def list_current_directory() -> list[str]:
# return os.listdir()
# @mcp.tool(description="Crea un archivo con un nombre dado.")
# def create_file(filename: str) -> str:
# with open(filename, "w") as f:
# f.write("")
# return f"Archivo '{filename}' creado."
# @mcp.tool(description="Lee el contenido de un archivo de texto dado.")
# def read_file(filename: str) -> str:
# with open(filename, "r") as f:
# return f.read()
# @mcp.tool(description="Escribe contenido a un archivo, sobrescribiéndolo.")
# def write_file(filename: str, content: str) -> str:
# with open(filename, "w") as f:
# f.write(content)
# return f"Contenido escrito en '{filename}'."
@mcp.tool(description="Devuelve el número de CPUs disponibles en el sistema.")
def get_cpu_count() -> int:
return os.cpu_count()
@mcp.tool(description="Devuelve el timestamp actual (UNIX).")
def current_timestamp() -> float:
return datetime.datetime.now().timestamp()
if __name__ == "__main__":
mcp.run(transport="streamable-http", host="127.0.0.1", port=4300, path="/tools")