Skip to main content
Glama
navigation.py13 kB
""" Herramientas de navegación para el vault de Obsidian Incluye funciones para listar, leer y buscar notas """ from datetime import date, datetime from pathlib import Path from fastmcp import FastMCP from ..config import get_vault_path from ..utils import find_note_by_name, get_note_metadata def register_navigation_tools(mcp: FastMCP) -> None: """ Registra todas las herramientas de navegación en el servidor MCP Args: mcp: Instancia del servidor FastMCP """ @mcp.tool() def listar_notas(carpeta: str = "", incluir_subcarpetas: bool = True) -> str: """ Lista todas las notas (.md) en el vault o en una carpeta específica Args: carpeta: Carpeta específica a explorar (vacío = raíz del vault) incluir_subcarpetas: Si incluir subcarpetas en la búsqueda """ try: vault_path = get_vault_path() if not vault_path: return "❌ Error: La ruta del vault no está configurada." if carpeta: target_path = vault_path / carpeta if not target_path.exists(): return f"❌ La carpeta '{carpeta}' no existe en el vault" else: target_path = vault_path # Buscar archivos markdown pattern = "**/*.md" if incluir_subcarpetas else "*.md" notas = list(target_path.glob(pattern)) if not notas: return f"📂 No se encontraron notas en '{carpeta or 'raíz'}'" # Organizar por carpetas notas_por_carpeta: dict[str, list[dict]] = {} for nota in notas: ruta_relativa = nota.relative_to(vault_path) carpeta_padre = ( str(ruta_relativa.parent) if ruta_relativa.parent != Path(".") else "📄 Raíz" ) if carpeta_padre not in notas_por_carpeta: notas_por_carpeta[carpeta_padre] = [] metadata = get_note_metadata(nota) notas_por_carpeta[carpeta_padre].append(metadata) # Formatear resultado resultado = f"📚 Notas encontradas en el vault ({len(notas)} total):\n\n" for carpeta_nombre, lista_notas in sorted(notas_por_carpeta.items()): resultado += f"📁 {carpeta_nombre} ({len(lista_notas)} notas):\n" for nota_meta in sorted(lista_notas, key=lambda x: x["name"]): resultado += ( f" 📄 {nota_meta['name']} " f"({nota_meta['size_kb']:.1f}KB, {nota_meta['modified']})\n" ) resultado += "\n" return resultado except Exception as e: return f"❌ Error al listar notas: {e}" @mcp.tool() def leer_nota(nombre_archivo: str) -> str: """ Lee el contenido completo de una nota específica Args: nombre_archivo: Nombre del archivo (ej: "Diario/2024-01-01.md") """ try: nota_path = find_note_by_name(nombre_archivo) if not nota_path: return f"❌ No se encontró la nota '{nombre_archivo}'" # Leer contenido with open(nota_path, "r", encoding="utf-8") as f: contenido = f.read() # Obtener metadata metadata = get_note_metadata(nota_path) resultado = f"📄 **{metadata['name']}**\n" resultado += f"📍 Ubicación: {metadata['relative_path']}\n" resultado += ( f"📊 Tamaño: {metadata['size_kb']:.1f}KB | " f"Modificado: {metadata['modified']}\n" ) resultado += f"{'=' * 50}\n\n" resultado += contenido return resultado except Exception as e: return f"❌ Error al leer nota: {e}" @mcp.tool() def buscar_en_notas( texto: str, carpeta: str = "", solo_titulos: bool = False ) -> str: """ Busca texto en las notas del vault Args: texto: Texto a buscar carpeta: Carpeta específica donde buscar (vacío = todo el vault) solo_titulos: Si buscar solo en los títulos de las notas """ try: vault_path = get_vault_path() if not vault_path: return "❌ Error: La ruta del vault no está configurada." if carpeta: search_path = vault_path / carpeta if not search_path.exists(): return f"❌ La carpeta '{carpeta}' no existe" else: search_path = vault_path resultados = [] archivos_revisados = 0 for archivo_item in search_path.rglob("*.md"): archivos_revisados += 1 try: ruta_relativa = archivo_item.relative_to(vault_path) if solo_titulos: # Buscar solo en el nombre del archivo if texto.lower() in archivo_item.stem.lower(): resultados.append( { "archivo": str(ruta_relativa), "tipo": "título", "coincidencia": archivo_item.stem, } ) else: # Buscar en todo el contenido with open(archivo_item, "r", encoding="utf-8") as f: contenido = f.read() lineas = contenido.split("\n") for num_linea, linea in enumerate(lineas, 1): if texto.lower() in linea.lower(): coincidencia_texto = linea.strip() if len(coincidencia_texto) > 100: coincidencia_texto = ( coincidencia_texto[:100] + "..." ) resultados.append( { "archivo": str(ruta_relativa), "linea": str(num_linea), "coincidencia": coincidencia_texto, } ) except Exception: continue if not resultados: busqueda_tipo = "títulos" if solo_titulos else "contenido" return ( f"🔍 No se encontró '{texto}' en {busqueda_tipo} " f"de {archivos_revisados} notas" ) # Formatear resultados busqueda_tipo = "títulos" if solo_titulos else "contenido" resultado = ( f"🔍 Búsqueda de '{texto}' en {busqueda_tipo} " f"({len(resultados)} coincidencias):\n\n" ) # Agrupar por archivo por_archivo: dict[str, list[dict]] = {} for r in resultados: archivo_res = r["archivo"] if archivo_res not in por_archivo: por_archivo[archivo_res] = [] por_archivo[archivo_res].append(r) for archivo, coincidencias in list(por_archivo.items())[ :20 ]: # Limitar a 20 archivos resultado += f"📄 **{archivo}** ({len(coincidencias)} coincidencias):\n" for coincidencia in coincidencias[ :5 ]: # Máximo 5 coincidencias por archivo if solo_titulos: resultado += f" 📌 {coincidencia['coincidencia']}\n" else: resultado += ( f" 📍 Línea {coincidencia['linea']}: " f"{coincidencia['coincidencia']}\n" ) if len(coincidencias) > 5: resultado += ( f" ... y {len(coincidencias) - 5} coincidencias más\n" ) resultado += "\n" if len(por_archivo) > 20: resultado += ( f"... y {len(por_archivo) - 20} archivos más con coincidencias" ) return resultado except Exception as e: return f"❌ Error en búsqueda: {e}" @mcp.tool() def buscar_notas_por_fecha(fecha_desde: str, fecha_hasta: str = "") -> str: """ Busca notas modificadas en un rango de fechas Args: fecha_desde: Fecha de inicio (YYYY-MM-DD) fecha_hasta: Fecha de fin (YYYY-MM-DD, opcional, por defecto hoy) """ try: vault_path = get_vault_path() if not vault_path: return "❌ Error: La ruta del vault no está configurada." # Parsear fechas fecha_inicio = datetime.strptime(fecha_desde, "%Y-%m-%d").date() if fecha_hasta: fecha_fin = datetime.strptime(fecha_hasta, "%Y-%m-%d").date() else: fecha_fin = date.today() notas_encontradas = [] for archivo in vault_path.rglob("*.md"): fecha_mod = datetime.fromtimestamp(archivo.stat().st_mtime).date() if fecha_inicio <= fecha_mod <= fecha_fin: metadata = get_note_metadata(archivo) metadata["fecha"] = fecha_mod.strftime("%Y-%m-%d") notas_encontradas.append(metadata) if not notas_encontradas: return ( f"📅 No se encontraron notas modificadas entre " f"{fecha_desde} y {fecha_fin}" ) # Ordenar por fecha (más recientes primero) notas_encontradas.sort(key=lambda x: x["fecha"], reverse=True) resultado = ( f"📅 Notas modificadas entre {fecha_desde} y {fecha_fin} " f"({len(notas_encontradas)} encontradas):\n\n" ) for nota in notas_encontradas: resultado += f"📄 {nota['name']} ({nota['size_kb']:.1f}KB)\n" resultado += f" 📍 {nota['relative_path']} | 📅 {nota['fecha']}\n\n" return resultado except ValueError: return "❌ Formato de fecha inválido. Usa YYYY-MM-DD (ej: 2024-01-15)" except Exception as e: return f"❌ Error al buscar por fecha: {e}" @mcp.tool() def mover_nota(origen: str, destino: str, crear_carpetas: bool = True) -> str: """ Mueve o renombra una nota dentro del vault. Args: origen: Ruta relativa actual de la nota (ej: "Sin titulo.md") destino: Ruta relativa nueva de la nota (ej: "01_Inbox/Nueva Nota.md") crear_carpetas: Si crear las carpetas destino si no existen (True) Returns: Mensaje de éxito o error. """ try: vault_path = get_vault_path() if not vault_path: return "❌ Error: Ruta del vault no configurada" # Validaciones de seguridad para carpeta PRIVADO # No permitir mover nada HACIA ni DESDE "04_Recursos/Privado" ruta_prohibida = "04_Recursos/Privado" if ruta_prohibida in str(origen) or ruta_prohibida in str(destino): return ( f"⛔ ACCESO DENEGADO: No se permite mover archivos hacia/desde " f"{ruta_prohibida}" ) path_origen = vault_path / origen path_destino = vault_path / destino # Verificar origen if not path_origen.exists(): return f"❌ El archivo origen no existe: {origen}" if not path_origen.is_file(): return f"❌ El origen no es un archivo: {origen}" # Verificar destino if path_destino.exists(): return f"❌ El archivo destino ya existe: {destino}" # Crear carpetas si es necesario if crear_carpetas: path_destino.parent.mkdir(parents=True, exist_ok=True) elif not path_destino.parent.exists(): return f"❌ La carpeta destino no existe: {path_destino.parent.name}" # Mover archivo path_origen.rename(path_destino) return f"✅ Archivo movido/renombrado:\nDe: {origen}\nA: {destino}" except Exception as e: return f"❌ Error al mover nota: {e}"

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Vasallo94/obsidian-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server