Arquitectura
Vista General
Sección titulada «Vista General»mcp-memory sigue una arquitectura en capas con tres componentes principales: el servidor MCP (transporte + tools), la capa de almacenamiento (SQLite) y el motor de embeddings (ONNX). Una cuarta capa — el motor de Limbic Scoring — se sitúa sobre el almacenamiento y orquesta el re-ranking dinámico.
graph TD Client["MCP Client<br/>(Claude Desktop, OpenCode, Cursor)"] Client -->|stdio JSON-RPC| FastMCP
subgraph FastMCP["FastMCP Server"] Tools["19 MCP Tools<br/>(8 Anthropic + 11 extended)"] Pydantic["Pydantic Validation"] Engine["EmbeddingEngine<br/>(ONNX, lazy load)"] Tools --> Pydantic Tools --> Engine end
subgraph Storage["Storage Layer"] MemoryStore["MemoryStore<br/>(SQLite + sqlite-vec + FTS5)"] SQLite["memory.db<br/>(WAL mode)"] end
subgraph Scoring["Scoring Layer"] Limbic["scoring.py (Limbic)<br/>Saliencia · Decay · Co-oc · Temporal Co-oc Decay"] Routing["Query Router<br/>(COSINE/LIMBIC/HYBRID)"] end
subgraph EntitySplit["Entity Splitting"] Splitter["entity_splitter.py<br/>(TF-IDF + Thresholds)"] end
subgraph Scripts["Scripts"] AutoTuner["auto_tuner.py<br/>(Grid Search + NDCG@K)"] GridSearch["grid_search.py<br/>(Offline GAMMA/BETA_SAL)"] ABMetrics["ab_metrics.py<br/>(Shadow Mode Metrics)"] end
Pydantic --> MemoryStore Engine --> MemoryStore MemoryStore --> SQLite MemoryStore --> Limbic Limbic --> RoutingEl servidor arranca como un proceso stdio que escucha JSON-RPC en stdin y responde vía stdout. Los logs van a stderr para no interferir con el protocolo MCP.
Ruta de almacenamiento por defecto: ~/.config/opencode/mcp-memory/memory.db
Stack Tecnológico
Sección titulada «Stack Tecnológico»| Componente | Tecnología | Versión | Propósito |
|---|---|---|---|
| Runtime | Python | >= 3.12 | Versión mínima del lenguaje |
| Servidor MCP | FastMCP | >= 3.0 | Framework para registro de tools y transporte stdio |
| Base de datos | SQLite | stdlib | Almacenamiento persistente con journaling WAL |
| Búsqueda vectorial | sqlite-vec | >= 0.1.6 | KNN con distancia coseno sobre tabla virtual vec0 |
| Embeddings | ONNX Runtime | >= 1.17 | Inferencia en CPU para embeddings de oraciones |
| Tokenización | HuggingFace Tokenizers | >= 0.19 | Tokenización rápida (implementación en Rust) |
| Numéricos | NumPy | >= 1.26 | Operaciones vectoriales y álgebra lineal |
| Validación | Pydantic | >= 2.0 | Validación de modelos de entrada/salida |
| Descarga de modelo | HuggingFace Hub | >= 0.20 | Descarga de modelos desde HF Hub |
| Build system | hatchling | — | Packaging de Python |
Transporte
Sección titulada «Transporte»mcp-memory se comunica por stdio usando JSON-RPC:
- Entrada: el servidor lee peticiones JSON-RPC desde stdin
- Salida: las respuestas se escriben en stdout
- Logs: toda la salida diagnóstica va a stderr — nunca stdout — para no contaminar el canal del protocolo MCP
El entry point se registra como mcp-memory, que resuelve a mcp_memory.server:main. Esto lo hace compatible con cualquier cliente MCP que soporte transporte stdio:
{ "mcpServers": { "memory": { "command": ["uvx", "--from", "git+https://github.com/Yarlan1503/mcp-memory", "mcp-memory"] } }}Componentes Principales
Sección titulada «Componentes Principales»El código se organiza en cinco módulos centrales:
server.py — Servidor FastMCP
Sección titulada «server.py — Servidor FastMCP»El entry point y registro de tools — solo 97 líneas. Inicializa el servidor FastMCP, crea la instancia de MemoryStore y registra 19 tools importando desde cinco módulos (tools/core.py, tools/search.py, tools/entity_mgmt.py, tools/reflections.py, tools/relations.py). Cada tool tiene entradas y salidas validadas por Pydantic. El store se comparte vía un patrón de runtime lookup — las funciones de tool acceden a él mediante _server_mod.store en lugar de closure o global.
storage.py — MemoryStore
Sección titulada «storage.py — MemoryStore»La capa de persistencia — organizada como un package con 7 mixins. MemoryStore en storage/__init__.py es una clase facade que hereda de CoreMixin, SchemaMixin, RelationsMixin, SearchMixin, AccessMixin, ReflectionsMixin y ConsolidationMixin. Cada mixin maneja un dominio específico (CRUD de entidades, migraciones de schema, relaciones, búsqueda, tracking de accesos, reflexiones, consolidación). Todos comparten una única conexión SQLite (self.db) vía MRO (Method Resolution Order) de Python.
embeddings.py — EmbeddingEngine
Sección titulada «embeddings.py — EmbeddingEngine»Encapsula toda la lógica de inferencia de embeddings usando un modelo ONNX. Implementa un patrón singleton con carga diferida — el modelo solo se carga en memoria en su primer uso. Proporciona el pipeline de codificación: prepended de prefijo → tokenización → forward pass ONNX → mean pooling → normalización L2.
scoring.py — Limbic Scoring
Sección titulada «scoring.py — Limbic Scoring»El motor de ranking dinámico. Calcula scores compuestos a partir de tres señales: salience (frecuencia de acceso + grado en el grafo), decay temporal (exponencial con half-life configurable) y co-ocurrencia (frecuencia con la que entidades aparecen juntas en resultados). También implementa Reciprocal Rank Fusion para fusionar resultados KNN y FTS5. Consulta Sistema Límbico para más detalles.
entity_splitter.py — División de Entidades
Sección titulada «entity_splitter.py — División de Entidades»Divide automáticamente entidades que exceden los umbrales de observaciones en sub-entidades enfocadas. Usa TF-IDF para agrupar observaciones por tópico y crea relaciones contiene/parte_de para preservar la estructura del knowledge graph.
- Umbrales:
Sesion=15,Proyecto=25,DEFAULT=20 - Extracción de tópicos: TF-IDF con stop words en español, longitud mínima de palabra 4
- Propuesta de split: Retorna nuevas entidades sugeridas + relaciones a crear
- Ejecución atómica: Transacción todo-o-nada vía SQLite context manager
scripts/auto_tuner.py — Auto-tuning
Sección titulada «scripts/auto_tuner.py — Auto-tuning»Script de optimización offline para hiperparámetros GAMMA y BETA_SAL:
- Grid search: Explora combinaciones de GAMMA × BETA_SAL
- Métricas NDCG@K: Usa feedback implícito del A/B testing en shadow mode
- Aplicación suave: Media móvil exponencial para evitar cambios súbitos
- CLI:
--analyze,--tune,--set-gamma,--set-beta
scripts/ab_metrics.py — Métricas de A/B Testing
Sección titulada «scripts/ab_metrics.py — Métricas de A/B Testing»Calcula métricas de calidad desde datos de shadow mode:
- NDCG@K: Normalized Discounted Cumulative Gain
- Lift@K: Proporción de items relevantes en top-K vs total
- Comparación baseline: Treatment vs ranking solo coseno
migrate.py — Importación JSONL
Sección titulada «migrate.py — Importación JSONL»Importa datos del formato JSONL de Anthropic a SQLite. Procesa el archivo línea por línea, tolera entradas corruptas y genera embeddings en batch al final. Completamente idempotente — ejecutarlo múltiples veces produce el mismo resultado. Consulta la Guía de Migración para el proceso completo.
config.py — Límites de Input
Sección titulada «config.py — Límites de Input»Constantes para validación de inputs en las tools. Define MAX_OBS=100, MAX_ENTITIES=50, MAX_OBS_LEN=2000 y MAX_QUERY_LEN=500 para prevenir abuso y mantener rendimiento. También contiene la configuración de A/B testing (USE_AB_TESTING, BASELINE_PROBABILITY).
_helpers.py — Helpers Compartidos
Sección titulada «_helpers.py — Helpers Compartidos»Tres funciones utilitarias compartidas entre módulos de tools: _get_engine() (carga diferida del singleton EmbeddingEngine), _format_output() (estandariza el formato de salida de entidades) y _get_store() (recupera la instancia de MemoryStore desde el módulo del servidor). Centraliza el patrón de runtime lookup usado por todas las tools.
retry.py — Manejo de Concurrencia
Sección titulada «retry.py — Manejo de Concurrencia»El decorator retry_on_locked proporciona backoff exponencial con jitter para operaciones de escritura SQLite bajo concurrencia multi-cliente. Se aplica a los 21 métodos de escritura en la capa de storage. Parámetros: max_retries=5, base_delay=0.1s, max_delay=2.0s, con 10% de jitter para prevenir thundering herd. Esencial cuando múltiples clientes MCP (ej: dos sesiones opencode) escriben a la misma base de datos simultáneamente. Desde v2.2, cada reintento realiza automáticamente un rollback() antes de reintentar la operación, asegurando un estado de transacción limpio.
Flujo de Datos: Escritura (create_entities)
Sección titulada «Flujo de Datos: Escritura (create_entities)»Cuando un cliente invoca create_entities, los datos fluyen por cuatro etapas antes de persistirse con su embedding semántico:
sequenceDiagram participant C as Client participant F as FastMCP participant P as Pydantic participant S as MemoryStore participant E as EmbeddingEngine participant V as sqlite-vec
C->>F: create_entities([{name, entityType, observations}]) F->>P: EntityInput.model_validate(dict) Note over P: Validates name (non-empty),<br/>entityType, observations P->>S: upsert_entity(name, type) S->>S: add_observations(entity_id, obs)<br/>[dedup by content] S->>E: prepare_entity_text(name, type, obs) Note over E: Head+Tail+Diversity<br/>selection, 480 token budget E->>E: encode([text]) → float[384] E->>V: INSERT OR REPLACE<br/>(rowid, embedding) V-->>C: Entity created with embeddingPaso a paso:
- Cliente → FastMCP: el cliente envía una petición JSON-RPC con una lista de diccionarios de entidades
- FastMCP → Pydantic: cada diccionario se valida contra
EntityInput— el nombre debe ser no vacío, las observaciones por defecto son[] - Pydantic → MemoryStore: la entidad se inserta o actualiza (
INSERT ... ON CONFLICT(name) DO UPDATE). Las observaciones se añaden con deduplicación por contenido exacto - MemoryStore → EmbeddingEngine: el texto de la entidad se prepara usando la estrategia Head+Tail+Diversity (presupuesto de 480 tokens) y se codifica en un vector de 384 dimensiones
- EmbeddingEngine → sqlite-vec: el vector (1.536 bytes como float32) se almacena con
INSERT OR REPLACEusando el ID de la entidad como rowid
Flujo de Datos: Búsqueda
Sección titulada «Flujo de Datos: Búsqueda»La búsqueda semántica usa un pipeline híbrido: la búsqueda vectorial KNN se ejecuta en paralelo con la búsqueda de texto completo FTS5, los resultados se fusionan vía Reciprocal Rank Fusion y luego se reordenan por el motor de Limbic Scoring.
sequenceDiagram participant C as Client participant F as FastMCP participant KNN as KNN (sqlite-vec) participant FTS as FTS5 (BM25) participant RRF as RRF Merge participant L as Limbic Scoring participant S as MemoryStore
C->>F: search_semantic("project memory", limit=10) F->>F: Check engine.available
par Parallel Search F->>KNN: encode(query) → KNN 3× limit F->>FTS: BM25 on name/type/obs end
KNN-->>RRF: [{id, distance}] FTS-->>RRF: [{id, rank}] Note over RRF: score(d) = Σ 1/(k + rank_i)<br/>k = 60 RRF->>L: Fused candidates + scores L->>L: Fetch access, degree,<br/>co-occurrence data L->>L: Compute limbic_score<br/>per candidate L->>S: Top-K entity IDs (hydrate) S->>S: get_entity_by_id() +<br/>get_observations() Note over S: record_access() +<br/>record_co_occurrences()<br/>(post-response, best-effort) S-->>C: Results with limbic_score<br/>+ scoring breakdownPaso a paso:
- Cliente → FastMCP: la cadena de consulta y el límite opcional llegan vía JSON-RPC
- Verificación de disponibilidad: si el modelo ONNX no está cargado, el servidor retorna un error claro con instrucciones de descarga
- Búsqueda en paralelo: dos ramas se ejecutan simultáneamente:
- KNN: la consulta se codifica con el prefijo
"query: "y se compara contra los vectores almacenados vía sqlite-vec (recupera 3× el límite de candidatos) - FTS5: ranking BM25 sobre nombres de entidades, tipos y texto de observaciones
- KNN: la consulta se codifica con el prefijo
- Fusión RRF: los resultados de ambas ramas se fusionan usando Reciprocal Rank Fusion (
k=60). Las entidades que aparecen en ambos rankings reciben un boost combinado en el score - Re-ranking Límbico: los candidatos fusionados se puntúan mediante el Sistema Límbico, que aplica salience, decaimiento temporal y boosts de co-ocurrencia. El query routing (
detect_query_type()) determina la estrategia (COSINE_HEAVY/LIMBIC_HEAVY/HYBRID_BALANCED) basándose en características lingüísticas y k_limit. - Hidratación: los IDs de las top-K entidades se hidratan con los datos completos (nombre, tipo, observaciones) desde SQLite
- Tracking post-respuesta: los eventos de acceso y co-ocurrencias se registran para mejorar rankings futuros — esto es best-effort y no bloquea la respuesta
Para detalles sobre cómo funciona la fórmula de Limbic Scoring, consulta Sistema Límbico.
Carga Diferida (Lazy Loading)
Sección titulada «Carga Diferida (Lazy Loading)»EmbeddingEngine usa un patrón singleton + carga diferida para mantener el inicio rápido:
graph TD A["Server starts<br/>EmbeddingEngine._instance = None"] --> B["First call to get_instance()"] B --> C{Model files exist<br/>in ~/.cache/mcp-memory-v2/models/?} C -->|Yes| D["Load ONNX + Tokenizer<br/>_available = True"] C -->|No| E["_available = False<br/>Server continues without embeddings"] D --> F["Ready for search_semantic<br/>and embedding generation"]El servidor arranca sin cargar el modelo. Las 8 tools compatibles con Anthropic funcionan inmediatamente usando solo SQLite, al igual que la mayoría de las tools extendidas. El motor de embeddings se inicializa bajo demanda — la primera vez que una tool lo necesita.
Carga diferida en dos niveles:
- Nivel de import:
mcp_memory.embeddingsse importa dentro de_get_engine(), no en el scope del módulo enserver.py - Nivel de instancia:
EmbeddingEngine.get_instance()crea el singleton en la primera llamada
| Escenario | Comportamiento |
|---|---|
| Modelo descargado | La primera llamada a search_semantic toma ~3-5 segundos (carga). Llamadas subsiguientes: milisegundos |
| Modelo no descargado | search_semantic retorna un error claro. Las otras 10 tools funcionan con normalidad |
| sqlite-vec no disponible | El servidor continúa sin búsqueda vectorial. Las operaciones CRUD no se ven afectadas |
Base de Datos
Sección titulada «Base de Datos»Ruta por Defecto
Sección titulada «Ruta por Defecto»~/.config/opencode/mcp-memory/memory.dbEl directorio se crea automáticamente si no existe. Un único archivo contiene todos los datos — entidades, observaciones, relaciones, embeddings y metadatos de scoring.
Modo WAL y Concurrencia
Sección titulada «Modo WAL y Concurrencia»SQLite se configura con Write-Ahead Logging (WAL) para acceso concurrente seguro:
PRAGMA journal_mode = WAL # Lecturas concurrentes sin bloquear escriturasPRAGMA busy_timeout = 10000 # Esperar hasta 10s si está bloqueadoPRAGMA synchronous = NORMAL # Balance entre seguridad y velocidadPRAGMA cache_size = -64000 # 64 MB de caché de páginasPRAGMA temp_store = MEMORY # Tablas temporales en RAMPRAGMA foreign_keys = ON # Aplicar integridad referencial| Operación | Comportamiento |
|---|---|
| Lecturas concurrentes | Permitidas — WAL soporta múltiples lectores simultáneos |
| Escrituras | Secuenciales — un solo escritor, pero los lectores no se bloquean |
| Contención de locks | Los escritores esperan hasta 10 segundos (busy_timeout) por un lock |
Desde v2.2, las operaciones de escritura usan retry_on_locked — backoff exponencial con jitter que maneja errores database is locked de forma transparente. Cada reintento realiza un rollback() automático antes de reintentar, y las operaciones de escritura largas (como add_observations) usan BEGIN IMMEDIATE para adquirir el lock de escritura de antemano. Esto permite acceso multi-cliente seguro (ej: dos sesiones opencode escribiendo concurrentemente) sin lógica de reintento manual.
Resumen del Schema
Sección titulada «Resumen del Schema»| Tabla | Tipo | Propósito |
|---|---|---|
entities | Regular | Nodos del grafo (id, name, entity_type, timestamps) |
observations | Regular | Hechos adjuntos a entidades (entity_id FK, content) |
relations | Regular | Aristas del grafo (from_entity, to_entity, relation_type) |
db_metadata | Regular | Metadatos clave-valor del sistema |
entity_embeddings | Virtual (vec0) | Vectores de 384 dimensiones con distancia coseno |
entity_fts | Virtual (FTS5) | Búsqueda de texto completo con ranking BM25 |
entity_access | Regular | Registro de accesos para Limbic Scoring |
co_occurrences | Regular | Registro de co-ocurrencias para Limbic Scoring |
Para el schema DDL completo, definiciones de índices y detalles de modelos Pydantic, consulta la Referencia de API.
A/B Testing: Shadow Mode
Sección titulada «A/B Testing: Shadow Mode»MCP Memory v2 incluye un sistema de testing A/B en shadow mode que compara scoring límbico contra un baseline solo coseno sin afectar la experiencia del usuario.
Cómo Funciona
Sección titulada «Cómo Funciona»| Aspecto | Descripción |
|---|---|
| Shadow mode | Cada llamada a search_semantic ejecuta ambos rankings (baseline y límbico) |
| Asignación | Determinista por hash (texto de query → bucket) o aleatoria (10% baseline) |
| Logging | Tablas search_events y search_results almacenan rankings crudos |
| Métricas | ab_metrics.py computa NDCG@K, Lift@K desde datos loggeados |
| Sin impacto al usuario | Resultados baseline se loguean pero nunca se retornan |
Configuración
Sección titulada «Configuración»USE_AB_TESTING = TrueBASELINE_PROBABILITY = 0.1 # 10% de queries son baselineTablas de Base de Datos
Sección titulada «Tablas de Base de Datos»| Tabla | Propósito |
|---|---|
search_events | Metadata de query: texto, treatment, k_limit, timestamp, duración |
search_results | Datos de ranking por entidad: entity_id, rank, limbic_score, cosine_sim |
implicit_feedback | Eventos de re-acceso para cálculo de NDCG |
Workflow de Auto-Tuning
Sección titulada «Workflow de Auto-Tuning»- Recolectar datos de shadow mode vía uso normal de
search_semantic - Ejecutar
python scripts/auto_tuner.py --tunecuando se acumule suficiente data - El script encuentra GAMMA × BETA_SAL óptimos vía grid search
- Aplica suavemente vía media móvil exponencial (blend_factor=0.1)
- Actualiza tanto
db_metadatacomo las constantes enscoring.py
Convenciones de Diseño
Sección titulada «Convenciones de Diseño»Varias decisiones arquitectónicas distinguen a mcp-memory del servidor original de Anthropic y soluciones similares:
SQLite sobre JSONL
Sección titulada «SQLite sobre JSONL»El servidor original de Anthropic reescribe todo el knowledge graph a un archivo JSONL en cada operación, sin locking. Esto causa corrupción de datos bajo acceso concurrente. SQLite con modo WAL proporciona transacciones ACID, consultas indexadas (O(log n) vs recorrido lineal) y concurrencia segura — sin requerir un servidor de base de datos separado.
ONNX sobre APIs Cloud
Sección titulada «ONNX sobre APIs Cloud»Los embeddings se ejecutan localmente vía ONNX Runtime en CPU. Sin API keys, sin latencia de red, sin rate limits, sin vendor lock-in. El tradeoff es ~465 MB de espacio en disco para el modelo y ~5ms por codificación — aceptable para una herramienta local.
Distancia Coseno, 384 Dimensiones
Sección titulada «Distancia Coseno, 384 Dimensiones»El modelo intfloat/multilingual-e5-small produce vectores de 384 dimensiones. Este es un balance deliberado entre calidad y huella:
- 384 dims × 4 bytes = 1.536 bytes por embedding — suficientemente pequeño para almacenamiento eficiente y búsqueda KNN rápida
- Distancia coseno (
d = 1 - cos(A, B)) coincide con el entrenamiento del modelo e5 - Los vectores se normalizan con L2 antes de almacenarse, permitiendo que el producto escalar sirva como proxy de la similitud coseno
Over-Retrieval + Re-Ranking
Sección titulada «Over-Retrieval + Re-Ranking»La búsqueda recupera 3× el límite solicitado de KNN (ej: 30 candidatos para limit=10), y luego reordena con Limbic Scoring para producir el top-K final. Esto le da al motor de scoring un pool más amplio para trabajar, mejorando la calidad de los resultados sin overhead significativo.
Prefijos Asimétricos
Sección titulada «Prefijos Asimétricos»El modelo e5 requiere prefijos específicos por tarea para una recuperación óptima:
- Las consultas usan el prefijo
"query: " - Las entidades (pasajes) usan el prefijo
"passage: "
Esto es un requisito de la metodología de entrenamiento del modelo — usar el prefijo incorrecto degrada significativamente la calidad de búsqueda.
Embeddings No Incrementales
Sección titulada «Embeddings No Incrementales»Los embeddings se regeneran desde cero cada vez que cambia el contenido de una entidad. Aunque esto cuesta una codificación completa cada vez, garantiza que el vector siempre refleja el estado actual — sin actualizaciones parciales obsoletas ni artefactos de acumulación.
Auto-Tuning vía Grid Search
Sección titulada «Auto-Tuning vía Grid Search»Las constantes del Limbic Scoring (GAMMA, BETA_SAL) son ajustables vía grid search offline:
- Fuente de datos: Logs de A/B testing en shadow mode (no requiere labeling manual)
- Métrica: NDCG@K (Normalized Discounted Cumulative Gain at K)
- Proceso:
auto_tuner.py --tuneexplora combinaciones, aplica suavemente - Persistencia: Tanto tabla
db_metadatacomo constantes del móduloscoring.py
Esto permite mejora continua sin cambios de código o reinicios del servicio.