MCP Platform

Entorno de ejecución MCP local con orquestación multiagente, servidores de herramientas distribuidos y recomendaciones multimedia impulsadas por ML.

Documentación

MCP Platform Tests

Runtime MCP local con orquestación multi-agente, servidores de herramientas distribuidos, recomendaciones multimedia impulsadas por ML, memoria persistente entre sesiones y un programador de agentes proactivo.

⚠️ Experimental — destinado únicamente para uso personal y experimental, no para implementación en producción.


Tabla de Contenidos


Prerrequisitos

  • Python 3.12+
  • 16GB+ de RAM recomendado
  • Ollama instalado

1. Inicio Rápido

Instalar Dependencias

cd mcp-platform
python3 -m venv .venv
source .venv/bin/activate       # Linux/macOS
.venv\Scripts\activate          # Windows PowerShell
pip install -r requirements.txt

Nota sobre WSL2: crear .venv dentro de /mnt/c/ falla porque NTFS no admite los enlaces simbólicos Unix que requieren los venvs. Crea el venv en el sistema de archivos de Linux en su lugar:

python3 -m venv /home/$USER/.virtualenvs/mcp_platform
source /home/$USER/.virtualenvs/mcp_platform/bin/activate
pip install -r requirements.txt

Backend LLM

curl -fsSL https://ollama.com/install.sh | sh
OLLAMA_HOST=0.0.0.0:11434 ollama serve
ollama pull qwen2.5:14b-instruct-q4_K_M   # primary inference (recommended — any tool-calling model works)
ollama pull qwen2.5:0.5b                  # routing classifier (fast pre-flight intent detection)
ollama pull bge-large                     # RAG document embeddings
ollama pull qwen3-vl:8b-instruct          # vision / image analysis

Alternativa GGUF: los archivos GGUF locales (por ejemplo, de Hugging Face) también funcionan: descarga el archivo y ejecuta :gguf add <path_to_file> en el prompt. No se necesita editar configuración.

Modelos sentence-transformers (embeddings de memoria + reranker RAG) se descargan automáticamente en el primer uso; no se necesita descarga manual.

Iniciar el Cliente

python client.py

Accede a la interfaz web en: http://localhost:9000/client/ui/index.html


2. Uso de Servidores MCP con Otros Clientes

Agrega a tu configuración de cliente MCP (por ejemplo, claude_desktop_config.json):

{
    "mcpServers": {
        "code_assistant": {
            "command": "/path/to/mcp-platform/.venv/bin/python",
            "args": ["/path/to/mcp-platform/servers/code_assistant/server.py"]
        }
    }
}

Rutas de Windows: "command": "C:\\path\\to\\mcp-platform\\.venv\\Scripts\\python.exe"

Servidores disponibles:

  • code_assistant - Análisis, generación y refactorización de código impulsado por IA (12 herramientas)
  • code_review - Revisión de código, búsqueda y corrección de errores (3 herramientas)
  • code_runner - Sandbox de ejecución de Python/bash (4 herramientas)
  • discord - Notificaciones de canales de Discord mediante webhook (2 herramientas) ⚠️ Requiere DISCORD_WEBHOOK_URL
  • github - Clonar, explorar y limpiar repositorios de GitHub (4 herramientas) ⚠️ Requiere GITHUB_TOKEN para repositorios privados
  • google - Gmail + Google Calendar (13 herramientas) ⚠️ Requiere configuración de Google (Apps Script u OAuth — consulta Configuración de Google)
  • image - Búsqueda, análisis y generación de imágenes con IA (6 herramientas) ⚠️ Requiere SERPER_API_KEY para búsqueda; la generación es gratuita
  • location - Clima, hora, ubicación (3 herramientas) — usa Open-Meteo (gratuito, sin clave); recurre a OpenWeatherMap si OPENWEATHER_API_KEY está configurado
  • plex - Biblioteca multimedia + recomendaciones ML (18 herramientas) ⚠️ Requiere PLEX_URL, PLEX_TOKEN
  • rag - Búsqueda y gestión vectorial (8 herramientas) ⚠️ Requiere Ollama + bge-large
  • system - Información del sistema y procesos (3 herramientas)
  • text - Procesamiento de texto y búsqueda web (8 herramientas)
  • trilium - Integración de notas Trilium (11 herramientas) ⚠️ Requiere TRILIUM_URL, TRILIUM_TOKEN

3. Configuración del Cliente

Variables de Entorno

Crea .env en la raíz del proyecto:

# === LLM Backend ===
OLLAMA_BASE_URL=http://127.0.0.1:11434  # Use 127.0.0.1 for local; LAN IP requires OLLAMA_HOST=0.0.0.0 on the server
OLLAMA_VISION_MODEL=qwen3-vl:8b-instruct
MAX_MESSAGE_HISTORY=30
LLM_MESSAGE_WINDOW=15        # Sliding window of messages sent to LLM; older turns fall into Conversation RAG
LLM_TEMPERATURE=0.3
OLLAMA_NUM_CTX=8192          # KV cache / context window size
OLLAMA_NUM_PREDICT=4096      # Max tokens the LLM will generate per response
OLLAMA_REPEAT_PENALTY=1.1    # Penalise token repetition (1.0 = disabled)
IMAGE_MODEL=flux             # Model used for AI image generation

# === GGUF Configuration ===
GGUF_GPU_LAYERS=-1
GGUF_CONTEXT_SIZE=4096
GGUF_BATCH_SIZE=512

# === API Keys ===
PLEX_URL=http://localhost:32400
PLEX_TOKEN=your_token_here
TRILIUM_URL=http://localhost:8888
TRILIUM_TOKEN=your_token_here
SHASHIN_BASE_URL=http://localhost:6624/
SHASHIN_API_KEY=your_key_here
SERPER_API_KEY=your_key_here
OLLAMA_TOKEN=your_token_here
LANGSEARCH_API_KEY=your_key_here   # fallback search when Ollama weekly limit is reached
OPENWEATHER_API_KEY=your_key_here  # fallback weather when Open-Meteo is unavailable
DISCORD_WEBHOOK_URL=your_webhook_url_here

# === Google Apps Script (alternative to OAuth for Gmail/Calendar) ===
# Paste PASTE_INTO_GOOGLE_APPS_SCRIPT.js into script.google.com, deploy as a Web App,
# then set the URL with your SECRET_KEY appended as ?key=...
# When set, ALL Google tools use the script instead of OAuth.
GOOGLE_APPS_SCRIPT_URL=https://script.google.com/macros/s/.../exec?key=<strong-random-secret-32-chars>

# === A2A Protocol ===
A2A_ENDPOINTS=http://localhost:8010
A2A_EXPOSED_TOOLS=

# === Performance Tuning ===
CONCURRENT_LIMIT=3
EMBEDDING_BATCH_SIZE=50
DB_FLUSH_BATCH_SIZE=50

# === Tool Control ===
DISABLED_TOOLS=plex:*

# === Location ===
DEFAULT_CITY=Vancouver
DEFAULT_STATE=BC
DEFAULT_COUNTRY=Canada
DEFAULT_TIMEZONE=America/Vancouver

Configuración Recomendada

ollama pull qwen2.5:14b-instruct-q4_K_M   # primary inference — any tool-calling model works; 14b q4 is a good default
ollama pull qwen2.5:0.5b                  # routing classifier — small/fast; set LLM_ROUTING_MODEL=qwen2.5:0.5b in .env
ollama pull bge-large                     # required for RAG
ollama pull qwen3-vl:8b-instruct          # required for image tools
# sentence-transformers models (memory embeddings + reranker) auto-download on first use

Archivos GGUF: descarga cualquier GGUF (por ejemplo, de Hugging Face) y ejecuta :gguf add <path_to_file> en el prompt; no se necesita editar configuración.

.env mínimo para comenzar:

OLLAMA_VISION_MODEL=qwen3-vl:8b-instruct
DISABLED_TOOLS=plex:*,image_tools:shashin_analyze,shashin_random,shashin_search
OLLAMA_TOKEN=<token>
SERPER_API_KEY=<key>

Requisitos de Funciones

FunciónVariables de entorno requeridasConfiguración adicional
Gmail + Google Calendar—Configuración de Google — Apps Script (más simple) u OAuth (consulta Configuración de Google)
Notificaciones de DiscordDISCORD_WEBHOOK_URLCrear webhook en la configuración del canal de Discord
Ingestión y búsqueda RAG—Ollama en ejecución + bge-large descargado
Biblioteca multimedia de PlexPLEX_URL, PLEX_TOKENPlex Media Server en ejecución
Búsqueda web (principal)OLLAMA_TOKENCuenta de Ollama + clave API
Búsqueda web (respaldo)LANGSEARCH_API_KEYCuenta de LangSearch — panel. Se usa automáticamente cuando se alcanza el límite semanal de Ollama o devuelve vacío.
Búsqueda de imágenesSERPER_API_KEYCuenta de Serper + clave API
Generación de imágenes con IA—Gratis mediante Pollinations.ai — no se requiere clave
Notas TriliumTRILIUM_URL, TRILIUM_TOKENServidor Trilium en ejecución
Galería de fotos ShashinSHASHIN_BASE_URL, SHASHIN_API_KEYServidor Shashin en ejecución
Modo distribuido A2AA2A_ENDPOINTSServidor A2A remoto en ejecución

Comandos Disponibles

:jobs                  - List all scheduled jobs
:jobs pause <label>    - Pause a scheduled job
:jobs enable <label>   - Resume a scheduled job
:jobs cancel <label>   - Delete a scheduled job
:jobs info <label>     - Show full job detail
:memory                        - List all memories
:memory semantic               - List permanent memories only
:memory episodic               - List session-derived memories
:memory forget <id>            - Delete a memory by ID
:memory clear                  - Clear all episodic memories
:memory clear session <id>     - Delete memories from one session
:memory consolidate <id>       - Extract memories from a session now
:memory add <fact>             - Manually add a permanent memory
:memory dedup                  - Remove duplicate memories
:commands              - List all available commands
:clear sessions        - Clear all chat history
:clear session <id>    - Clear session
:sessions              - List all sessions
:stop                  - Stop current operation
:stats                 - Show performance metrics
:tools                 - List available tools
:tools --all           - Show all tools including disabled
:tool <n>              - Get tool description
:model                 - List all available models
:model <n>             - Switch to a model
:gguf add <path>       - Register a GGUF model
:gguf remove <alias>   - Remove a GGUF model
:gguf list             - List registered GGUF models
:a2a on/off/status     - Control A2A mode
:health                - Health overview of all servers
:env                   - Show environment configuration

Configuración de Google

Opción A — Google Apps Script (configuración más simple)

  1. Ve a https://script.google.com y crea un nuevo proyecto
  2. Elimina todo el código existente y pega el contenido de PASTE_INTO_GOOGLE_APPS_SCRIPT.js
  3. Reemplaza <SECRET_KEY> en el script con un secreto aleatorio fuerte (32+ caracteres). Genera uno: python3 -c "import secrets; print(secrets.token_urlsafe(32))" Cualquiera que conozca esta clave puede leer tus correos y calendario: trátala como una contraseña.
  4. Haz clic en Implementar > Nueva implementación > Aplicación web
    • Ejecutar como: Yo
    • Quién tiene acceso: Cualquier persona
    • Haz clic en Implementar y copia la URL de la aplicación web
  5. Agrega a .env:
    GOOGLE_APPS_SCRIPT_URL=<Web App URL>?key=<your SECRET_KEY>
    

En ediciones posteriores del script, usa Implementar > Administrar implementaciones y edita la implementación existente; no crees una nueva o la URL cambiará.

Opción B — Google OAuth

Configuración única. Después de completar estos pasos, el servidor se ejecuta sin intervención.

  1. Ve a https://console.cloud.google.com/ y crea un proyecto
  2. Habilita Gmail API y Google Calendar API
  3. Crea un cliente de Aplicación de escritorio OAuth y descarga credentials.json
  4. Colócalo en servers/google/credentials.json
  5. Publica la aplicación en Producción — esto es obligatorio. Las aplicaciones en modo "Prueba" tienen tokens que expiran cada 7 días, lo que causa errores invalid_grant en trabajos programados
  6. Ejecuta: .venv/bin/python auth_google.py
  7. Reinicia: python client.py

Si el token expira más tarde: la plataforma lo detecta en la siguiente llamada de herramienta de Google y muestra un banner de reautorización en la interfaz con un enlace. Haz clic en el enlace, aprueba el acceso, pega el código de autorización en el chat — no se necesita reiniciar el servidor. Alternativamente, elimina servers/google/token.json y vuelve a ejecutar el paso 6.


4. Agregar Herramientas Personalizadas

Paso 1: Crea el archivo del servidor

mkdir servers/my_tool && touch servers/my_tool/server.py

Paso 2: Implementa con @tool_meta

from mcp.server.fastmcp import FastMCP
from tools.tool_control import check_tool_enabled
from client.tool_meta import tool_meta

mcp = FastMCP("my-tool-server")

@mcp.tool()
@check_tool_enabled(category="my_tool")
@tool_meta(
    tags=["read", "search"],
    triggers=["my keyword", "my phrase"],
    template='use my_function: arg1=""',
)
def my_function(arg1: str) -> str:
    """Short description."""
    return json.dumps({"content": f"Processed {arg1}"})

if __name__ == "__main__":
    mcp.run(transport="stdio")

Reinicia el cliente y la herramienta estará activa: enrutada, etiquetada y registrada automáticamente.

Referencia de campos @tool_meta

CampoRequeridoDescripción
tags✅Etiquetas de capacidades
triggers✅Frases en lenguaje natural que enrutan a esta herramienta
templaterecomendadoTexto de relleno previo mostrado en el panel de herramientas
text_fieldssi es necesarioCampos de respuesta que contienen el texto principal
rate_limitno"100/hour", "10/day", "ollama", o None
idempotentnoTrue si no tiene efectos secundarios (predeterminado: True)
intent_categorynoAnula el nombre del grupo de enrutamiento

Vocabulario de etiquetas

EtiquetaSignificado
readSolo lee datos
writeCrea o modifica datos
destructiveElimina o cambia datos de forma irreversible
searchEl propósito principal es la búsqueda
externalLlama a una API externa
visionProcesa entrada de imágenes
mediaOpera sobre audio/video/imagen
calendarInteractúa con datos de calendario
emailInteractúa con correo electrónico
notesInteractúa con sistemas de toma de notas
codeOpera sobre código fuente
systemInteractúa con el sistema operativo o hardware
ragInteractúa con el almacén vectorial RAG
aiLlama a un modelo LLM o ML

Agregar Servidores MCP Externos

Crea external_servers.json en la raíz del proyecto:

{
    "external_servers": {
        "deepwiki": {
            "transport": "sse",
            "url": "https://mcp.deepwiki.com/mcp",
            "enabled": true
        }
    }
}

Transportes admitidos: sse, http, stdio. Convención de variable de entorno para autenticación de encabezado:

ES_SERVERNAME_TOKEN=your_token_here

5. Modo Distribuido (Protocolo A2A)

python a2a_server.py    # Terminal 1 — starts on http://localhost:8010
python client.py        # Terminal 2
A2A_ENDPOINTS=http://localhost:8010,http://gpu-server:8020
A2A_EXPOSED_TOOLS=plex,location,text   # empty = expose all

Todos los endpoints configurados se descubren y registran simultáneamente al inicio mediante asyncio.gather() — los tiempos de espera de conexión para endpoints inalcanzables ya no se bloquean entre sí.


6. Pruebas

Ejecutar Pruebas

Activa tu virtualenv primero, luego ejecuta desde la raíz del proyecto:

source .venv/bin/activate        # Linux/macOS
.venv\Scripts\activate           # Windows PowerShell
# WSL2: source /home/$USER/.virtualenvs/mcp_platform/bin/activate

python -m pytest                 # all tests
python -m pytest -m unit         # fast unit tests only
python -m pytest -m integration  # integration tests
python -m pytest -m e2e          # end-to-end tests
python -m pytest --no-cov        # skip coverage (faster)
python -m pytest -x              # stop on first failure
python -m pytest -k "session"    # filter by name

Estructura de Pruebas

tests/
├── conftest.py
├── unit/         <- fast isolated unit tests
├── integration/  <- multi-component tests
└── e2e/          <- full conversation tests

tests/results/    <- generated after running tests
├── junit.xml
├── coverage.xml
├── test-report.html
└── coverage-report.html

CI/CD

Las pruebas se ejecutan automáticamente en cada push y pull request mediante GitHub Actions (.github/workflows/ci.yml). En caso de fallo, se abre automáticamente un Issue de GitHub con un enlace a la ejecución fallida.

Para subir la cobertura a Codecov, agrega al flujo de trabajo:

- name: Upload coverage
  uses: codecov/codecov-action@v3
  with:
    files: results/coverage.xml

7. Arquitectura

Diseño Multi-Servidor

servers/
├── code_assistant/   12 tools  - AI-powered code analysis, generation, and refactoring
├── code_review/       3 tools  - Code review, search, and bug fixing
├── code_runner/       4 tools  - Python/bash execution sandbox
├── discord/           2 tools  - Discord channel notifications [requires DISCORD_WEBHOOK_URL]
├── github/            4 tools  - GitHub repo clone, browse, and cleanup
├── google/           13 tools  - Gmail + Google Calendar       [requires Google setup]
├── image/             6 tools  - Image search, analysis, AI generation
├── location/          3 tools  - Weather, time, location
├── plex/             18 tools  - Media + ML recommendations    [requires PLEX_URL + PLEX_TOKEN]
├── rag/               8 tools  - Vector search and management  [requires Ollama + bge-large]
├── system/            3 tools  - System info and processes
├── text/              8 tools  - Text processing and web search
└── trilium/          11 tools  - Trilium notes integration     [requires TRILIUM_URL + TRILIUM_TOKEN]

Total: 95 herramientas en 13 servidores

Concurrencia y Paralelismo

La plataforma usa asyncio.gather() en varias capas para ejecutar trabajo no LLM de forma concurrente:

CapaQué se ejecuta en paralelo
Inicio — descubrimiento de servidoresVerificaciones de alcance TCP y sondeos OAuth para todos los servidores externos
Inicio — registro A2ATodos los A2A_ENDPOINTS se descubren y registran simultáneamente
Ejecución de tareas multi-agenteLas tareas independientes (sin dependencias pendientes) se ejecutan como un lote concurrente en cada ciclo
Ejecución de subtareas A2AMismo lote agrupado con detección de dependencias que el modo multi-agente
Transmisión WebSocketTodos los clientes conectados reciben mensajes mediante una sola llamada gather()

Nota de hardware: La inferencia LLM se serializa en la capa GPU de Ollama independientemente de la concurrencia: una inferencia se ejecuta a la vez en una sola GPU. El beneficio del paralelismo está en el trabajo limitado por I/O: llamadas de herramientas HTTP, consultas de bases de datos y solicitudes de red. La ejecución LLM verdaderamente paralela requeriría múltiples GPUs o subagentes alojados en la nube.

Estructura de Directorios

mcp-platform/
├── servers/
├── a2a_server.py
├── client.py
├── client/
│   ├── ui/
│   ├── capability_registry.py  <- auto-populated from @tool_meta
│   ├── langgraph.py
│   ├── memory_consolidator.py  <- persistent cross-session memory
│   ├── proactive_agent.py      <- scheduler + condition triggers
│   ├── query_patterns.py       <- auto-populated from @tool_meta triggers
│   ├── tool_meta.py            <- single source of truth for tool metadata
│   ├── websocket.py
│   └── ...
├── data/
│   ├── sessions.db             <- session + message history
│   ├── memory.db               <- persistent memory (created on first run)
│   └── scheduler.db            <- scheduled jobs (created on first run)
└── tools/

8. RAG y Memoria de Conversación

Cómo funciona el contexto

Cada llamada LLM recibe contexto ensamblado en este orden exacto:

System prompt
  ├─ Persistent memory — top 5 by importance (always injected)
  ├─ Persistent memory — query-relevant (vector search, above threshold)
  └─ Original system instructions + session ID

Conversation RAG — turns that scrolled out of the window (if relevant)

Message window — last LLM_MESSAGE_WINDOW turns (always injected)

Current user message

Cadena de búsqueda — qué sucede en cada consulta:

  1. Memoria persistente (siempre) — las 5 memorias de mayor importancia se inyectan incondicionalmente en el prompt del sistema, independientemente de la relevancia de la consulta. Esto asegura que los hechos centrales (tu nombre, familia, preferencias) nunca se pierdan incluso cuando ninguna consulta obtiene buena puntuación. Se agregan memorias adicionales relevantes a la consulta mediante búsqueda vectorial.

  2. Ventana de mensajes (siempre) — los últimos LLM_MESSAGE_WINDOW turnos de la sesión actual se incluyen directamente como historial de conversación.

  3. RAG de conversación (siempre) — se ejecuta una búsqueda semántica contra los turnos que han salido de la ventana. Los resultados por encima del umbral del reranker se inyectan entre el prompt del sistema y el historial.

  4. El LLM genera la respuesta usando todo lo anterior.

¿Cuándo te salva cada capa?

SituaciónQué ayuda
Preguntado sobre algo de hace 3 mensajesVentana de mensajes
Preguntado sobre algo de hace 20 mensajes, misma sesiónRAG de conversación
Preguntado sobre tu nombre en una nueva sesiónMemoria persistente (ancla)
Preguntado sobre un hecho específico de una sesión antiguaMemoria persistente (relevante a la consulta)
Preguntado sobre un documento o artículo específicoRAG de conversación
Hecho aún no consolidado en memoriaNada — dile a la plataforma nuevamente

Ventana de conversación (LLM_MESSAGE_WINDOW)

Controla cuántos turnos recientes ve el LLM directamente. Se establece en .env:

LLM_MESSAGE_WINDOW=15   # default: 6, recommended: 15

Una ventana de 6 es demasiado ajustada para una conversación normal: la información compartida al inicio de una sesión se sale del contexto antes de que puedas preguntar sobre ella. 15 cubre un intercambio completo sin alcanzar los límites de tokens en qwen2.5:14b. Si compartes algo y el LLM parece olvidarlo unos mensajes después, aumenta este valor.

Ingestión de desbordamiento

Cuando el historial supera la ventana, los turnos más antiguos se ingieren automáticamente en la base de datos vectorial RAG como pares Human + Assistant. Siguen siendo buscables mediante similitud semántica incluso después de salir de la ventana.

Recuperación automática RAG

En cada mensaje, se ejecuta una búsqueda semántica contra todo el almacén RAG utilizando el mensaje actual del usuario como consulta. Los fragmentos coincidentes (de turnos de conversación antiguos, documentos ingeridos, subtítulos de Plex o investigaciones) se inyectan en el contexto automáticamente, sin necesidad de un desencadenante explícito de search rag.

Para qué sirve cada método de recuperación

Tipo de consultaMejor herramienta
"¿Qué discutimos sobre X?"Auto-RAG (semántico)
"¿Cuál fue mi primer mensaje?"session_history_tool (base de datos ordenada)
"Resume esta sesión"session_history_tool
"¿Qué decía ese artículo que leímos?"Auto-RAG
"¿Qué pregunté hace 10 mensajes?"session_history_tool

session_history_tool

Añadido al servidor rag. Consulta la base de datos SQLite de la sesión directamente para obtener un historial de mensajes ordenado y con marca de tiempo. El ID de sesión actual siempre se inyecta en el mensaje del sistema para que el LLM pueda pasarlo automáticamente.

use session_history_tool: session_id="<id>" [limit="20"] [order="asc"]

Desencadenantes: first prompt, what did I ask, earlier in this session, summarise this session, session history


9. Memoria Persistente y Agentes Proactivos

Cómo funcionan juntos la memoria y el contexto

La plataforma tiene tres capas de contexto, cada una con un propósito diferente:

CapaQué esAlcance¿Sobrevive al borrado de sesión?
Ventana de mensajesÚltimos N turnos en el contexto directo del LLMSesión actualNo
RAG de conversaciónTurnos antiguos ingeridos como vectoresPor sesiónNo
Memoria persistenteHechos destilados extraídos por el LLMTodas las sesionesSí: sobrevive al borrado de sesión

Qué significa esto en la práctica: Si le dices a la plataforma el nombre de tu hijo y preguntas sobre ello 3 mensajes después, la ventana de mensajes lo maneja. Si preguntas 20 mensajes después, RAG lo maneja (normalmente). Si inicias una nueva sesión mañana, solo la memoria persistente lo tiene.

El flujo de trabajo de la memoria

Paso 1: Mantén una conversación. Dile a la plataforma cosas que quieras que recuerde: tu nombre, tu familia, tus proyectos, tus preferencias. Cuanto más declarativo, mejor ("El nombre de mi esposa es Suzy" vs "¿cuál es el nombre de mi esposa?").

Paso 2: La memoria se extrae automáticamente. Después de 15 minutos de inactividad, el InactivityWatcher se activa y ejecuta el LLM sobre la transcripción de tu sesión. Extrae hechos y los almacena en data/memory.db con incrustaciones vectoriales.

La reconsolidación es inteligente: rastrea el recuento de mensajes en la última consolidación y solo se vuelve a ejecutar si se han añadido nuevos mensajes desde entonces. Pasar la noche inactivo activa una extracción, no docenas.

Paso 3: Las memorias se inyectan en cada consulta. En cada nuevo mensaje ocurren dos cosas: las 5 memorias de mayor importancia siempre se inyectan en el mensaje del sistema de forma incondicional (para que tu nombre, familia y preferencias clave nunca se olviden), luego una búsqueda vectorial encuentra memorias adicionales relevantes a la consulta y las añade encima. El bloque combinado se ve así:

## Persistent Memory (from past sessions)
The following facts are KNOWN and TRUE. Use them to answer directly.

◆ The user's name is Bob
○ Bob's wife is Suzy, a nurse and excellent cook
○ Bob's son Sam is 11, plays accordion, excels at hockey
...

Paso 4: Las memorias se acumulan con el tiempo. Las memorias episódicas accedidas 3 o más veces se promueven al nivel semántico (permanente) cada noche. La plataforma se vuelve más útil cuanto más la usas.

Cuando la memoria no se activa automáticamente

El vigilante de inactividad se activa 15 minutos después de tu último mensaje. Si necesitas que las memorias se extraigan inmediatamente:

:memory consolidate <session_id>

Usa :sessions para encontrar el ID de sesión. El comando borra la marca de consolidación y vuelve a ejecutar la extracción independientemente del recuento de mensajes.

Comandos de memoria

:memory                        — list all memories (sorted by relevance)
:memory semantic               — permanent memories only
:memory episodic               — session-derived memories
:memory forget <id>            — delete one memory by ID
:memory clear                  — delete all episodic memories
:memory clear session <id>     — delete memories from one session
:memory consolidate <id>       — extract memories from a session now
:memory add <fact>             — manually add a permanent memory
:memory dedup                  — remove duplicate memories

Las memorias añadidas manualmente (:memory add) se almacenan en el nivel semantic con importancia 1.0: siempre ocupan el primer lugar en la recuperación.

Si el LLM olvida algo a mitad de sesión

Aumenta LLM_MESSAGE_WINDOW en .env. El valor predeterminado de 6 es demasiado ajustado: se recomienda 15. La información compartida al inicio de una sesión se sale de la ventana antes de que puedas preguntar sobre ella.

Una vez que un turno sale de la ventana, pasa a RAG de conversación: sigue ahí, pero ahora se recupera por similitud semántica en lugar de contexto directo. Esto significa que la redacción de la consulta debe ser lo suficientemente cercana al contenido original para que el reranker la muestre. Si el LLM aún no puede encontrar algo que se dijo antes en la misma sesión, intenta reformular la pregunta usando las mismas palabras clave que la declaración original.

Por ejemplo: si dijiste "Mi hijo Sam toca el acordeón" y luego preguntas "¿Qué instrumento toca mi hijo?", la coincidencia semántica es fuerte. Pero "¿Y Sam?" es demasiado vago para que RAG devuelva con confianza el hecho del acordeón: sé específico.

Dos niveles de memoria

NivelCómo se creaPersiste
episodicExtraído automáticamente de las sesionesPermanente: sobrevive al borrado de sesión
semanticPromovido desde episódico (3+ accesos) o añadido mediante :memory addPermanente

El umbral de promoción es configurable: MEMORY_PROMOTE_THRESHOLD=3 en .env.

Borrar una sesión no borra sus memorias. Los fragmentos RAG se purgan, pero los hechos extraídos permanecen. Para eliminar memorias de una sesión específica antes de borrarla:

:memory clear session <id>

Lectura de la salida de :memory

PERSISTENT MEMORY
────────────────────────────────────────────────
[11] The user's name is Bob
    ◆ semantic  |  importance: 1.0  |  accessed 163x
[22] Bob's wife Suzy is a nurse and an excellent cook.
    ○ episodic  |  importance: 0.9  |  accessed 89x

Cada entrada muestra:

  • [id] — el ID de memoria, usado con :memory forget <id> para eliminarla
  • Contenido — el hecho extraído, escrito en inglés sencillo
  • ◆ semantic / ○ episodic — nivel: semántico (permanente) o episódico (derivado de la sesión)
  • importance — puntuación asignada por el LLM en el momento de la extracción (0.0–1.0). Mayor = se recupera primero
  • accessed Nx — cuántas veces se devolvió esta memoria en una búsqueda de recuperación. Un recuento alto de accesos impulsa la promoción de episódico a semántico

Las memorias se ordenan por puntuación de recuperación (combinación de similitud vectorial e importancia), no por ID o fecha de creación. Las memorias más relevantes para tu última consulta aparecen primero.

Las memorias duplicadas pueden aparecer cuando la misma sesión se consolida varias veces, o cuando dos sesiones contienen hechos similares. No causan errores (el LLM ve ambas), pero desperdician espacio de contexto. Límpialas con:

:memory dedup        — removes exact duplicates, keeps highest importance copy
:memory forget <id>  — removes near-duplicates manually (same fact, different wording)

Memoria Persistente

La plataforma recuerda hechos, preferencias y resultados entre sesiones. Después de que una sesión termina (o después de 15 minutos de inactividad), el LLM extrae información memorable de la transcripción y la almacena en data/memory.db. En cada nueva sesión, las memorias relevantes se inyectan automáticamente en el mensaje del sistema: no se requiere volver a explicar.

Automatización Agéntica (Programador de Agentes Proactivos)

La plataforma puede actuar de forma autónoma o semiautónoma: ejecutando herramientas según un horario, sondeando condiciones e involucrando al LLM para interpretar los resultados antes de mostrártelos. Esta es la base para cosas como resúmenes diarios, monitoreo de correos electrónicos y, eventualmente, flujos de trabajo totalmente autónomos.

Solo describe lo que quieres:

show me my daily briefing every day at 6am
check for new emails every 5 minutes and summarize anything urgent
show me a random photo from my gallery every morning with a short commentary
alert me when I have unread emails
run the weather check every morning at 7am and write a friendly summary

El LLM clasifica si tu mensaje es una solicitud de programación, analiza la intención en una definición de trabajo y la presenta para confirmación antes de guardar nada. Las solicitudes ambiguas se aclaran con una pregunta en lugar de adivinarse.

Tres tipos de desencadenantes:

TipoCómo funcionaEjemplo
cronSe activa repetidamente en un horario fijo"todos los días a las 6am"
conditionSondea una herramienta de verificación en un intervalo; se activa solo cuando la condición es verdadera"cuando tenga correos no leídos"
onceSe activa una vez en una fecha/hora específica y luego se elimina"a las 8am hoy", "recuérdame mañana al mediodía"

Participación del LLM: Cada trabajo puede tener un llm_prompt: una instrucción pasada al LLM después de que la herramienta se ejecuta. En lugar de transmitir la salida bruta de la herramienta, el LLM la interpreta primero:

  • shashin_random_tool + "escribe un breve comentario sobre esta foto incluyendo la ubicación y el ambiente"
  • gmail_get_unread + "resume estos correos y marca cualquier cosa que necesite respuesta"
  • get_weather_tool + "escribe un resumen meteorológico matutino amigable"

Pipelines de herramientas: Encadena múltiples herramientas en secuencia usando >> en el llm_prompt. La salida de cada paso se pasa automáticamente al siguiente paso, sin necesidad de LLM:

use get_day_briefing >> use discord_notify
use gmail_get_unread >> use discord_notify
use calendar_get_today >> use discord_notify

Al programar, solo describe lo que quieres:

Every day at 7am, use get_day_briefing then use discord_notify to send me the result
At 8am today, use get_day_briefing then use discord_notify to send me today's schedule

El analizador del programador producirá automáticamente la sintaxis de cadena >>.

Las expresiones de condición tienen acceso a un contexto rico desde la salida JSON de la herramienta:

total_unread > 0       — fires when Gmail has unread emails
len_results > 0        — fires when a results list is non-empty
result > 10            — fires when a numeric value exceeds 10
result_len > 100       — fires when the raw output is non-trivial

Comandos de gestión de trabajos:

:jobs                    — list all scheduled jobs
:jobs pause <label>      — pause a job without deleting it
:jobs enable <label>     — resume a paused job
:jobs cancel <label>     — delete a job permanently
:jobs info <label>       — full job detail including cron and last run

Archivos:

  • client/memory_consolidator.py — extracción de memoria, almacenamiento, inyección y comandos :memory
  • client/proactive_agent.py — programador, desencadenantes de condición, analizador de intención basado en LLM y comandos :jobs

Dependencia requerida:

pip install apscheduler

Archivos de datos (creados automáticamente en el primer inicio):

data/memory.db      — persistent memory store
data/scheduler.db   — scheduled jobs store (label, tool, cron, llm_prompt, etc.)

10. Patrones de Intención y Solución de Problemas

Patrones de Intención

El enrutamiento está impulsado por el clasificador LLM y triggers en el decorador @tool_meta de cada herramienta: no se requiere edición manual de patrones. Los desencadenantes admiten cadenas simples y patrones regex con prefijo r:.

Forzar una herramienta específica:

Using shashin_search_tool, find photos of Sam
Using web_image_search_tool, show me a red panda

Derivación conversacional: Las consultas que el LLM clasifica como conversacionales (saludos, opiniones, escritura creativa) omiten el enrutamiento de herramientas por completo: se responden desde el contexto sin herramientas vinculadas.

Detección de programación: El LLM determina si un mensaje es una solicitud de programación/automatización, sin listas de palabras clave. Si se detecta, se enruta al ScheduleParser en lugar del agente.


Solución de Problemas

Ollama no accesible / conexión rechazada:

  • Usa OLLAMA_BASE_URL=http://127.0.0.1:11434 en .env para acceso local
  • Para WSL2 + acceso LAN, inicia Ollama con OLLAMA_HOST=0.0.0.0:11434 ollama serve
  • Añade proxies de puerto de Windows (PowerShell elevado):
    # Get WSL2 IP (use ip addr — hostname -I is not available on all WSL2 distros)
    $wslIp = (wsl -- ip -4 addr show eth0 | Select-String "inet ") -replace '.*inet (\d+\.\d+\.\d+\.\d+).*','$1'
    netsh interface portproxy add v4tov4 listenport=11434 listenaddress=0.0.0.0 connectport=11434 connectaddress=$wslIp
    New-NetFirewallRule -DisplayName "WSL2 Ollama" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 11434
    
  • La IP de WSL2 cambia al reiniciar: vuelve a ejecutar los comandos portproxy anteriores después de cada reinicio

Los modelos de Ollama no aparecen:

  • Si OLLAMA_BASE_URL apunta a una IP LAN pero Ollama está vinculado solo a 127.0.0.1, la lista de modelos estará vacía
  • Solución: inicia Ollama con OLLAMA_HOST=0.0.0.0:11434 ollama serve
  • Verifica: ollama list

Interfaz web no accesible desde LAN:

La plataforma vincula los puertos 9000 (HTTP), 8765 y 8766 (WebSockets) a 0.0.0.0 dentro del proceso. Si otros dispositivos en tu LAN pueden alcanzar esos puertos depende de tu entorno.

Linux nativo:

  • Debería funcionar sin configuración adicional: verifica que tu firewall permita tráfico entrante en esos puertos:
    sudo ufw allow 9000 && sudo ufw allow 8765 && sudo ufw allow 8766
    

WSL2 en Windows: WSL2 se ejecuta en una red virtual privada (172.x.x.x) que no es directamente enrutable desde tu LAN. Windows debe reenviar el tráfico LAN a WSL2 usando netsh portproxy. Ejecuta esto en un PowerShell elevado cada vez que la IP de WSL2 cambie (cambia en cada reinicio):

# 1. Find your current WSL2 IP (hostname -I is not available on all WSL2 distros)
$wslIp = (wsl -- ip -4 addr show eth0 | Select-String "inet ") -replace '.*inet (\d+\.\d+\.\d+\.\d+).*','$1'
Write-Host "WSL2 IP: $wslIp"

# 2. Add port forwarding for each port (both 0.0.0.0 and specific LAN IP — both are needed)
netsh interface portproxy add v4tov4 listenport=9000  listenaddress=0.0.0.0 connectport=9000  connectaddress=$wslIp
netsh interface portproxy add v4tov4 listenport=8765  listenaddress=0.0.0.0 connectport=8765  connectaddress=$wslIp
netsh interface portproxy add v4tov4 listenport=8766  listenaddress=0.0.0.0 connectport=8766  connectaddress=$wslIp
$lanIp = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.IPAddress -like "192.168.*" } | Select-Object -First 1).IPAddress
netsh interface portproxy add v4tov4 listenport=9000  listenaddress=$lanIp connectport=9000  connectaddress=$wslIp
netsh interface portproxy add v4tov4 listenport=8765  listenaddress=$lanIp connectport=8765  connectaddress=$wslIp
netsh interface portproxy add v4tov4 listenport=8766  listenaddress=$lanIp connectport=8766  connectaddress=$wslIp

# 3. Allow inbound through Windows Firewall (first time only)
New-NetFirewallRule -DisplayName "MCP Platform 9000" -Direction Inbound -Protocol TCP -LocalPort 9000 -Action Allow
New-NetFirewallRule -DisplayName "MCP Platform 8765" -Direction Inbound -Protocol TCP -LocalPort 8765 -Action Allow
New-NetFirewallRule -DisplayName "MCP Platform 8766" -Direction Inbound -Protocol TCP -LocalPort 8766 -Action Allow

# 4. Allow inbound through WSL2 firewall (first time only — run inside WSL2)
# sudo ufw allow 9000 && sudo ufw allow 8765 && sudo ufw allow 8766

Accede desde dispositivos LAN usando tu IP LAN de Windows (por ejemplo, 192.168.0.x), no la IP de WSL2:

http://192.168.0.x:9000/client/ui/index.html

Para encontrar tu IP LAN de Windows: ipconfig → busca la dirección 192.168.x.x en tu adaptador principal.

Consejo: si deja de funcionar después de un reinicio, solo vuelve a ejecutar los comandos netsh interface portproxy add: las reglas del firewall persisten, pero la IP de WSL2 habrá cambiado. La interfaz web no carga localmente:

netstat -an | grep LISTEN   # check ports 8765, 8766, 9000

La búsqueda web no devuelve resultados / respuestas vacías:

  • El nivel gratuito de Ollama tiene un límite de uso semanal: cuando se alcanza, la API devuelve HTTP 200 con un cuerpo vacío
  • La plataforma cambia automáticamente a LangSearch cuando esto ocurre
  • Establece LANGSEARCH_API_KEY en .env para habilitar el respaldo (cuenta gratuita en https://langsearch.com/dashboard)
  • Para confirmar qué proveedor se está utilizando, revisa los registros en busca de 🔍 Ollama web search frente a 🔍 LangSearch web search

Problemas con Google / OAuth:

  • Confirma que servers/google/token.json existe; si no, vuelve a ejecutar auth_google.py
  • Confirma que la API de Gmail y la API de Calendar están habilitadas en Google Cloud Console
  • Confirma que la aplicación OAuth está publicada en En producción (los tokens del modo de prueba caducan cada 7 días)
  • invalid_grant en tiempo de ejecución: el banner de re-autenticación aparecerá automáticamente en la interfaz — sigue el enlace, aprueba, pega el código en el chat

RAG no funciona:

  • Asegúrate de que Ollama esté en ejecución: OLLAMA_HOST=0.0.0.0:11434 ollama serve
  • Descarga bge-large si falta: ollama pull bge-large

Auto-RAG devolviendo contexto irrelevante:

  • Aumenta el umbral de min_score en la llamada a rag_search_tool dentro de run_agent
  • Verifica que el almacén RAG no esté contaminado: use rag_browse_tool
  • Elimina una fuente: use rag_delete_source_tool: source="<source>"

session_history_tool no encuentra mensajes:

  • Confirma que el ID de sesión en el prompt del sistema coincida con la sesión activa
  • Verifica que data/sessions.db exista y sea escribible

Las herramientas de Plex devuelven errores:

curl $PLEX_URL/identity?X-Plex-Token=$PLEX_TOKEN

El modelo GGUF no carga:

  • Reduce las capas de GPU: export GGUF_GPU_LAYERS=20
  • Solo CPU: export GGUF_GPU_LAYERS=0

El servidor A2A no se conecta:

curl http://localhost:8010/.well-known/agent-card.json

El historial de conversación no funciona:

  • Cambia a un modelo más grande: :model qwen2.5:14b-instruct-q4_K_M
  • Evita qwen2.5:3b / qwen2.5:7b para tareas que requieren contexto histórico

Las herramientas no aparecen:

:tools --all        # check if disabled
# check DISABLED_TOOLS in .env
python client.py    # restart

Licencia

Licencia MIT