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

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

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

  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

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

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

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

  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

:::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ó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.

Resumen del Schema

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

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

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

Configuración

USE_AB_TESTING = True
BASELINE_PROBABILITY = 0.1  # 10% de queries son baseline

Tablas de Base de Datos

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

Workflow de Auto-Tuning

  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

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.

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.