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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user