Ir al contenido

Arquitectura

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 --> 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

ComponenteTecnologíaVersiónPropósito
RuntimePython>= 3.12Versión mínima del lenguaje
Servidor MCPFastMCP>= 3.0Framework para registro de tools y transporte stdio
Base de datosSQLitestdlibAlmacenamiento persistente con journaling WAL
Búsqueda vectorialsqlite-vec>= 0.1.6KNN con distancia coseno sobre tabla virtual vec0
EmbeddingsONNX Runtime>= 1.17Inferencia en CPU para embeddings de oraciones
TokenizaciónHuggingFace Tokenizers>= 0.19Tokenización rápida (implementación en Rust)
NuméricosNumPy>= 1.26Operaciones vectoriales y álgebra lineal
ValidaciónPydantic>= 2.0Validación de modelos de entrada/salida
Descarga de modeloHuggingFace Hub>= 0.20Descarga de modelos desde HF Hub
Build systemhatchlingPackaging de Python

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"]
}
}
}

El código se organiza en cinco módulos centrales:

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.

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.

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.

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

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

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.

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).

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.

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.

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:

  1. Cliente → FastMCP: el cliente envía una petición JSON-RPC con una lista de diccionarios de entidades
  2. FastMCP → Pydantic: cada diccionario se valida contra EntityInput — el nombre debe ser no vacío, las observaciones por defecto son []
  3. 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
  4. 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
  5. EmbeddingEngine → sqlite-vec: el vector (1.536 bytes como float32) se almacena con INSERT OR REPLACE usando el ID de la entidad como rowid

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:

  1. Cliente → FastMCP: la cadena de consulta y el límite opcional llegan vía JSON-RPC
  2. Verificación de disponibilidad: si el modelo ONNX no está cargado, el servidor retorna un error claro con instrucciones de descarga
  3. 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
  4. 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
  5. 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.
  6. Hidratación: los IDs de las top-K entidades se hidratan con los datos completos (nombre, tipo, observaciones) desde SQLite
  7. 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.

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:

  1. Nivel de import: mcp_memory.embeddings se importa dentro de _get_engine(), no en el scope del módulo en server.py
  2. Nivel de instancia: EmbeddingEngine.get_instance() crea el singleton en la primera llamada
EscenarioComportamiento
Modelo descargadoLa primera llamada a search_semantic toma ~3-5 segundos (carga). Llamadas subsiguientes: milisegundos
Modelo no descargadosearch_semantic retorna un error claro. Las otras 10 tools funcionan con normalidad
sqlite-vec no disponibleEl servidor continúa sin búsqueda vectorial. Las operaciones CRUD no se ven afectadas
~/.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.

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ónComportamiento
Lecturas concurrentesPermitidas — WAL soporta múltiples lectores simultáneos
EscriturasSecuenciales — un solo escritor, pero los lectores no se bloquean
Contención de locksLos 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.

TablaTipoPropósito
entitiesRegularNodos del grafo (id, name, entity_type, timestamps)
observationsRegularHechos adjuntos a entidades (entity_id FK, content)
relationsRegularAristas del grafo (from_entity, to_entity, relation_type)
db_metadataRegularMetadatos clave-valor del sistema
entity_embeddingsVirtual (vec0)Vectores de 384 dimensiones con distancia coseno
entity_ftsVirtual (FTS5)Búsqueda de texto completo con ranking BM25
entity_accessRegularRegistro de accesos para Limbic Scoring
co_occurrencesRegularRegistro de co-ocurrencias para Limbic Scoring

Para el schema DDL completo, definiciones de índices y detalles de modelos Pydantic, consulta la Referencia de API.

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.

AspectoDescripción
Shadow modeCada llamada a search_semantic ejecuta ambos rankings (baseline y límbico)
AsignaciónDeterminista por hash (texto de query → bucket) o aleatoria (10% baseline)
LoggingTablas search_events y search_results almacenan rankings crudos
Métricasab_metrics.py computa NDCG@K, Lift@K desde datos loggeados
Sin impacto al usuarioResultados baseline se loguean pero nunca se retornan
USE_AB_TESTING = True
BASELINE_PROBABILITY = 0.1 # 10% de queries son baseline
TablaPropósito
search_eventsMetadata de query: texto, treatment, k_limit, timestamp, duración
search_resultsDatos de ranking por entidad: entity_id, rank, limbic_score, cosine_sim
implicit_feedbackEventos de re-acceso para cálculo de NDCG
  1. Recolectar datos de shadow mode vía uso normal de search_semantic
  2. Ejecutar python scripts/auto_tuner.py --tune cuando se acumule suficiente data
  3. El script encuentra GAMMA × BETA_SAL óptimos vía grid search
  4. Aplica suavemente vía media móvil exponencial (blend_factor=0.1)
  5. Actualiza tanto db_metadata como las constantes en scoring.py

Varias decisiones arquitectónicas distinguen a mcp-memory del servidor original de Anthropic y soluciones similares:

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.

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.

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

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.

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.

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.

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 --tune explora combinaciones, aplica suavemente
  • Persistencia: Tanto tabla db_metadata como constantes del módulo scoring.py

Esto permite mejora continua sin cambios de código o reinicios del servicio.