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/>(Clustering Semántico + c-TF-IDF fallback)"]
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 --> Routing
El 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
| 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 |
:::tip El consumo total de memoria es de ~500 MB — significativamente menor que soluciones similares que alcanzan ~1.4 GB. El modelo ONNX representa la mayor parte (~465 MB en disco). :::
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"]
}
}
}
:::note No se inicia ningún servidor HTTP. Toda la comunicación ocurre a través de pipes stdin/stdout, que es el transporte estándar de MCP para herramientas locales. :::
Componentes Principales
El código se organiza en cinco módulos centrales:
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
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
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
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
Divide automáticamente entidades que exceden los umbrales de observaciones en sub-entidades enfocadas. Usa clustering semántico (embeddings) como método principal de agrupación, con fallback a c-TF-IDF cuando los embeddings no están disponibles. Crea relaciones contiene/parte_de para preservar la estructura del knowledge graph.
- Umbrales:
Sesion=15,Proyecto=25,DEFAULT=20 - Extracción de tópicos: Clustering semántico + c-TF-IDF fallback 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
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
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
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
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
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
El decorator retry_on_locked proporciona backoff exponencial con jitter para operaciones de escritura SQLite bajo concurrencia multi-cliente. Se aplica a los 26 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)
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 embedding
Paso 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
:::caution El embedding se regenera desde cero cada vez que cambia el contenido de una entidad — no es incremental. Esto garantiza consistencia a cambio de una codificación completa (~5ms por vector en CPU). :::
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 breakdown
Paso 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 en un vector 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
- 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
:::tip Si FTS5 no retorna resultados, el pipeline recurre al modo semántico puro — usando solo candidatos KNN con re-ranking Límbico. Consulta Búsqueda Híbrida para los detalles completos de la implementación RRF. :::
Para detalles sobre cómo funciona la fórmula de Limbic Scoring, consulta Sistema Límbico.
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 |
:::note El inicio del servidor siempre toma ~1 segundo independientemente de si el modelo está descargado. El modelo ONNX de ~465 MB solo se carga en memoria cuando se necesita por primera vez. :::
Base de Datos
Ruta por Defecto
~/.config/opencode/mcp-memory/memory.db
El 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
SQLite se configura con Write-Ahead Logging (WAL) para acceso concurrente seguro:
PRAGMA journal_mode = WAL # Lecturas concurrentes sin bloquear escrituras
PRAGMA busy_timeout = 10000 # Esperar hasta 10s si está bloqueado
PRAGMA synchronous = NORMAL # Balance entre seguridad y velocidad
PRAGMA cache_size = -64000 # 64 MB de caché de páginas
PRAGMA temp_store = MEMORY # Tablas temporales en RAM
PRAGMA 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
| 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 |
:::caution
La tabla virtual vec0 no soporta ON DELETE CASCADE. Cuando se eliminan entidades, los embeddings deben removerse manualmente antes de la fila de la entidad — el código maneja esto automáticamente, pero tenlo en cuenta si escribes SQL directamente contra la base de datos.
:::
Para el schema DDL completo, definiciones de índices y detalles de modelos Pydantic, consulta la Referencia de API.
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
| 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
USE_AB_TESTING = True
BASELINE_PROBABILITY = 0.1 # 10% de queries son baseline
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
- 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
Varias decisiones arquitectónicas distinguen a mcp-memory del servidor original de Anthropic y soluciones similares:
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
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
El modelo sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2 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 MiniLM - 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
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.
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
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.