Esta página documenta el modelo de datos interno, el esquema de base de datos, la capa de validación Pydantic y la configuración del servidor de MCP Memory v2. Está dirigida a contribuidores que necesitan entender o modificar la implementación.

Configuración del servidor

ParámetroValor
Entry pointmcp-memorymcp_memory.server:main
Transportestdio
Logsstderr
Nombre MCP"memory"
Ruta DB por defecto~/.config/opencode/mcp-memory/memory.db
Caché de modelo~/.cache/mcp-memory-v2/models/

El directorio de la base de datos se crea automáticamente en la primera ejecución. Los archivos del modelo (ONNX + tokenizer) se descargan mediante scripts/download_model.py.

Diagrama Entidad-Relación

erDiagram
    entities ||--o{ observations : "has"
    entities ||--o{ relations : "from"
    entities ||--o{ relations : "to"
    entities ||--|| entity_embeddings : "1:1 (rowid)"
    entities ||--|| entity_access : "tracks"
    entities ||--o{ entity_access_log : "daily"
    entities ||--o{ co_occurrences : "co-occurs"
    entities ||--|| entity_fts : "indexed (FTS5)"
    entities ||--o{ search_results : "ranked"
    search_events ||--o{ search_results : "produces"
    search_events ||--o{ implicit_feedback : "receives"
    reflections ||--|| reflection_fts : "indexed (FTS5)"
    reflections ||--|| reflection_embeddings : "vector"

    entities {
        INTEGER id PK
        TEXT name UK
        TEXT entity_type
        TEXT status
        TEXT created_at
        TEXT updated_at
    }

    observations {
        INTEGER id PK
        INTEGER entity_id FK
        TEXT content
        TEXT kind
        INTEGER supersedes FK
        TEXT superseded_at
        INTEGER similarity_flag
        TEXT created_at
    }

    relations {
        INTEGER id PK
        INTEGER from_entity FK
        INTEGER to_entity FK
        TEXT relation_type
        TEXT context
        INTEGER active
        TEXT ended_at
        TEXT created_at
    }

    entity_embeddings {
        INTEGER rowid PK
        FLOAT embedding_384
    }

    entity_access {
        INTEGER entity_id PK_FK
        INTEGER access_count
        TEXT last_access
    }

    entity_access_log {
        INTEGER entity_id PK_FK
        TEXT access_date
        INTEGER access_count
    }

    co_occurrences {
        INTEGER entity_a_id FK
        INTEGER entity_b_id FK
        INTEGER co_count
        TEXT last_co
    }

    entity_fts {
        INTEGER rowid PK
        TEXT name
        TEXT entity_type
        TEXT obs_text
    }

    search_events {
        INTEGER event_id PK
        TEXT query_text
        TEXT query_hash
        TEXT timestamp
        INTEGER treatment
        INTEGER k_limit
        INTEGER num_results
        REAL duration_ms
        TEXT engine_used
    }

    search_results {
        INTEGER result_id PK
        INTEGER event_id FK
        INTEGER entity_id FK
        TEXT entity_name
        INTEGER rank
        REAL limbic_score
        REAL cosine_sim
        REAL importance
        REAL temporal
        REAL cooc_boost
        INTEGER baseline_rank
    }

    implicit_feedback {
        INTEGER feedback_id PK
        INTEGER event_id FK
        INTEGER entity_id
        INTEGER re_accessed
        INTEGER access_delta
        TEXT session_id
    }

    reflections {
        INTEGER id PK
        TEXT target_type
        INTEGER target_id
        TEXT author
        TEXT content
        TEXT mood
        TEXT created_at
    }

    reflection_fts {
        INTEGER rowid PK
        TEXT content
    }

    reflection_embeddings {
        INTEGER rowid PK
        FLOAT embedding_384
    }

    db_metadata {
        TEXT key PK
        TEXT value
    }

Esquema de base de datos

MCP Memory v2 almacena un knowledge graph en SQLite compuesto por tres elementos centrales — entities, observations y relations — extendidos por una cuarta capa de embeddings vectoriales (vía sqlite-vec) para búsqueda semántica.

  • Entities son nodos en el grafo.
  • Observations son hechos adjuntos a una entidad.
  • Relations conectan dos entidades con un enlace tipado.
  • Embeddings proyectan cada entidad en un espacio vectorial de 384 dimensiones para búsqueda por similitud coseno.

Tablas auxiliares adicionales alimentan el sistema de Limbic Scoring (entity_access, entity_access_log, co_occurrences), la búsqueda de texto completo (entity_fts), los tests A/B y el feedback implícito (search_events, search_results, implicit_feedback), y las reflexiones narrativas (reflections, reflection_fts, reflection_embeddings).

entities

La tabla principal del knowledge graph. Cada fila representa un nodo con un nombre único.

ColumnaTipoRestriccionesDescripción
idINTEGERPRIMARY KEY AUTOINCREMENTIdentificador interno único
nameTEXTNOT NULL UNIQUENombre legible de la entidad. Clave de negocio — dos entidades no pueden compartir nombre
entity_typeTEXTNOT NULL DEFAULT 'Generic'Clasificación de la entidad (ej. Sesion, Componente, Sistema)
statusTEXTNOT NULL DEFAULT 'activo'Estado del ciclo de vida de la entidad (ej. activo, archivado)
created_atTEXTNOT NULL DEFAULT (datetime('now'))Timestamp de creación en formato ISO-8601
updated_atTEXTNOT NULL DEFAULT (datetime('now'))Timestamp de última actualización

observations

Hechos o datos adjuntos a una entidad. Una entidad puede tener cero o muchas observaciones.

ColumnaTipoRestriccionesDescripción
idINTEGERPRIMARY KEY AUTOINCREMENTIdentificador interno único
entity_idINTEGERNOT NULL REFERENCES entities(id) ON DELETE CASCADEFK a la entidad padre. La eliminación en cascada borra las observaciones al eliminar la entidad
contentTEXTNOT NULLContenido de texto libre de la observación
kindTEXTNOT NULL DEFAULT 'generic'Categoría de la observación (ej. generic, milestone, risk)
supersedesINTEGERREFERENCES observations(id)FK a la observación que esta reemplaza
superseded_atTEXTTimestamp en que esta observación fue marcada como reemplazada
similarity_flagINTEGERNOT NULL DEFAULT 0Flag para flujos de detección de duplicados
created_atTEXTNOT NULL DEFAULT (datetime('now'))Timestamp de creación

:::note[Comportamiento CASCADE] ON DELETE CASCADE garantiza la integridad referencial: eliminar una entidad elimina automáticamente todas sus observaciones, previniendo registros huérfanos. :::

relations

Aristas que conectan dos entidades con un tipo de relación semántica.

ColumnaTipoRestriccionesDescripción
idINTEGERPRIMARY KEY AUTOINCREMENTIdentificador interno único
from_entityINTEGERNOT NULL REFERENCES entities(id) ON DELETE CASCADEFK a la entidad origen
to_entityINTEGERNOT NULL REFERENCES entities(id) ON DELETE CASCADEFK a la entidad destino
relation_typeTEXTNOT NULLTipo de relación (ej. uses, depends_on, part_of)
contextTEXTContexto narrativo opcional de la relación
activeINTEGERNOT NULL DEFAULT 1Flag de borrado lógico (1 = activa, 0 = finalizada)
ended_atTEXTTimestamp en que la relación fue marcada como finalizada
created_atTEXTNOT NULL DEFAULT (datetime('now'))Timestamp de creación

:::note[Restricción de unicidad] UNIQUE(from_entity, to_entity, relation_type) previene relaciones duplicadas del mismo tipo entre un par dado de entidades. :::

entity_embeddings (Virtual)

Tabla virtual implementada con la extensión sqlite-vec (vec0). Almacena el vector de embedding de cada entidad para alimentar la búsqueda semántica.

ColumnaTipoDescripción
embeddingfloat[384]Vector de 384 dimensiones generado por el modelo ONNX. Métrica de distancia: coseno
rowidINTEGER (implícito)Corresponde a entities.id. Vincula el embedding con su entidad

:::caution[vec0 y CASCADE] Las tablas virtuales vec0 no participan en ON DELETE CASCADE de SQLite. Al eliminar una entidad, los embeddings deben eliminarse manualmente antes de la fila de la entidad. El código base gestiona esto en delete_entities_by_names:

# 1. Eliminar embeddings primero (vec0 no soporta CASCADE)
self.db.execute(
    f"DELETE FROM entity_embeddings WHERE rowid IN ({id_placeholders})", ids
)
# 2. Eliminar entidad (CASCADE gestiona observations y relations)
self.db.execute(
    f"DELETE FROM entities WHERE id IN ({id_placeholders})", ids
)

Si eliminas entidades mediante SQL directo, limpia los embeddings primero para evitar registros huérfanos. :::

El enlace basado en rowid permite JOINs directos sin una columna FK explícita:

SELECT e.name, e.entity_type
FROM entities e
JOIN entity_embeddings ee ON e.id = ee.rowid
WHERE ee.embedding MATCH ?
ORDER BY distance;

db_metadata

Tabla auxiliar clave-valor para metadatos del sistema (versión del esquema, timestamp de última migración, configuración interna).

ColumnaTipoRestriccionesDescripción
keyTEXTPRIMARY KEYClave única del metadato
valueTEXTNOT NULLValor asociado

entity_access

Tabla de soporte para el sistema de Limbic Scoring. Registra la frecuencia y recencia con la que cada entidad aparece en resultados de search_semantic.

ColumnaTipoRestriccionesDescripción
entity_idINTEGERPRIMARY KEY REFERENCES entities(id) ON DELETE CASCADEFK a la entidad. Una fila por entidad
access_countINTEGERNOT NULL DEFAULT 1Número de veces que la entidad apareció en resultados de búsqueda semántica
last_accessTEXTNOT NULL DEFAULT (datetime('now'))Timestamp del último acceso (usado para decaimiento temporal)

co_occurrences

Tabla de soporte para el sistema de Limbic Scoring. Registra la frecuencia con la que dos entidades aparecen juntas en resultados de search_semantic.

ColumnaTipoRestriccionesDescripción
entity_a_idINTEGERNOT NULL REFERENCES entities(id) ON DELETE CASCADEFK a la entidad con el ID inferior (orden canónico)
entity_b_idINTEGERNOT NULL REFERENCES entities(id) ON DELETE CASCADEFK a la entidad con el ID superior
co_countINTEGERNOT NULL DEFAULT 1Número de co-ocurrencias registradas
last_coTEXTNOT NULL DEFAULT (datetime('now'))Timestamp de la última co-ocurrencia

:::note[Orden canónico] La clave primaria compuesta (entity_a_id, entity_b_id) exige que entity_a_id < entity_b_id siempre. Esto previene pares duplicados como (A, B) y (B, A). :::

entity_fts (FTS5 Virtual)

Tabla virtual FTS5 para búsqueda de texto completo. Indexa nombres de entidades, tipos y texto de observaciones concatenado.

ColumnaTipoDescripción
nameTEXTNombre de la entidad (buscable)
entity_typeTEXTTipo de entidad (buscable)
obs_textTEXTTodas las observaciones concatenadas con " | " como separador
rowidINTEGER (implícito)Corresponde a entities.id

El tokenizer es unicode61, que maneja correctamente caracteres acentuados (é, ñ, ü) y otros Unicode. La sincronización es a nivel de código (no triggers de SQLite): _sync_fts() se llama manualmente en upsert_entity, add_observations y delete_observations. En init_db(), si la tabla FTS está vacía pero existen entidades, _backfill_fts() la puebla.

entity_access_log

Seguimiento de accesos por día usado por el pipeline de consolidación. Una fila por entidad por día calendario.

ColumnaTipoRestriccionesDescripción
entity_idINTEGERNOT NULL REFERENCES entities(id) ON DELETE CASCADEFK a la entidad
access_dateTEXTNOT NULLFecha de acceso (ISO-8601)
access_countINTEGERNOT NULL DEFAULT 1Número de accesos en esa fecha

:::note[Clave compuesta] PRIMARY KEY (entity_id, access_date) fuerza una fila por entidad por día. :::

search_events

Tabla de logging de consultas para tests A/B y análisis de calidad de búsqueda.

ColumnaTipoRestriccionesDescripción
event_idINTEGERPRIMARY KEY AUTOINCREMENTIdentificador interno único
query_textTEXTNOT NULLCadena de consulta original
query_hashTEXTNOT NULLHash de la consulta para deduplicación
timestampTEXTNOT NULL DEFAULT (datetime('now'))Timestamp de ejecución de la consulta
treatmentINTEGERNOT NULLIdentificador del brazo del test A/B
k_limitINTEGERNOT NULLMáximo de resultados solicitados
num_resultsINTEGERNOT NULLNúmero real de resultados devueltos
duration_msREALLatencia de la consulta en milisegundos
engine_usedTEXTNOT NULLVariante del motor de búsqueda usado

search_results

Datos de ranking por entidad producidos por cada llamada a search_semantic.

ColumnaTipoRestriccionesDescripción
result_idINTEGERPRIMARY KEY AUTOINCREMENTIdentificador interno único
event_idINTEGERNOT NULL REFERENCES search_events(event_id)FK al evento de búsqueda padre
entity_idINTEGERNOT NULLFK a la entidad rankeada
entity_nameTEXTNOT NULLSnapshot del nombre de la entidad en el momento
rankINTEGERNOT NULLPosición en la lista de resultados (base 1)
limbic_scoreREALPuntaje final combinado
cosine_simREALSimilitud coseno raw
importanceREALComponente de conteo de accesos
temporalREALComponente de decaimiento temporal
cooc_boostREALBoost de co-ocurrencia
baseline_rankINTEGERRank sin reordenamiento limbic

:::note[Restricción de unicidad] UNIQUE(event_id, entity_id) previene filas duplicadas para la misma entidad en una búsqueda. :::

implicit_feedback

Registra eventos de re-acceso usados para calcular NDCG y otras métricas de calidad de búsqueda.

ColumnaTipoRestriccionesDescripción
feedback_idINTEGERPRIMARY KEY AUTOINCREMENTIdentificador interno único
event_idINTEGERNOT NULL REFERENCES search_events(event_id)FK al evento de búsqueda original
entity_idINTEGERNOT NULLFK a la entidad re-accedida
re_accessedINTEGERNOT NULL DEFAULT 0Si la entidad fue re-accedida (1 = sí)
access_deltaINTEGERSegundos entre búsqueda y re-acceso
session_idTEXTIdentificador de sesión opcional

reflections

Reflexiones narrativas que dan contexto y significado a los recuerdos.

ColumnaTipoRestriccionesDescripción
idINTEGERPRIMARY KEY AUTOINCREMENTIdentificador interno único
target_typeTEXTNOT NULLA qué apunta esta reflexión (entity, session, relation, global)
target_idINTEGERID de la entidad/relación objetivo (NULL para global)
authorTEXTNOT NULL DEFAULT 'sofia'Autor de la reflexión (nolan o sofia)
contentTEXTNOT NULLCuerpo de texto libre de la reflexión
moodTEXTTag de estado de ánimo opcional (frustracion, satisfaccion, etc.)
created_atTEXTNOT NULL DEFAULT (datetime('now'))Timestamp de creación

reflection_fts (FTS5 Virtual)

Tabla virtual FTS5 que indexa el contenido de las reflexiones para búsqueda de texto completo.

ColumnaTipoDescripción
contentTEXTTexto de la reflexión (buscable)
rowidINTEGER (implícito)Corresponde a reflections.id

reflection_embeddings (Virtual)

Tabla virtual implementada con sqlite-vec (vec0). Almacena embeddings vectoriales de las reflexiones para habilitar búsqueda semántica sobre contenido narrativo.

ColumnaTipoDescripción
embeddingfloat[384]Vector de 384 dimensiones. Métrica de distancia: coseno
rowidINTEGER (implícito)Corresponde a reflections.id. Vincula el embedding con su reflexión

Esquema SQL completo

El DDL completo incluyendo todas las tablas, tablas virtuales e índices:

CREATE TABLE entities (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    name        TEXT    NOT NULL UNIQUE,
    entity_type TEXT    NOT NULL DEFAULT 'Generic',
    status      TEXT    NOT NULL DEFAULT 'activo',
    created_at  TEXT    NOT NULL DEFAULT (datetime('now')),
    updated_at  TEXT    NOT NULL DEFAULT (datetime('now'))
);

CREATE TABLE observations (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    entity_id       INTEGER NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
    content         TEXT    NOT NULL,
    kind            TEXT    NOT NULL DEFAULT 'generic',
    supersedes      INTEGER REFERENCES observations(id),
    superseded_at   TEXT,
    similarity_flag INTEGER NOT NULL DEFAULT 0,
    created_at      TEXT    NOT NULL DEFAULT (datetime('now'))
);

CREATE TABLE relations (
    id            INTEGER PRIMARY KEY AUTOINCREMENT,
    from_entity   INTEGER NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
    to_entity     INTEGER NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
    relation_type TEXT    NOT NULL,
    context       TEXT,
    active        INTEGER NOT NULL DEFAULT 1,
    ended_at      TEXT,
    created_at    TEXT    NOT NULL DEFAULT (datetime('now')),
    UNIQUE(from_entity, to_entity, relation_type)
);

CREATE VIRTUAL TABLE entity_embeddings
USING vec0(embedding float[384] distance_metric=cosine);

CREATE TABLE db_metadata (
    key   TEXT PRIMARY KEY,
    value TEXT NOT NULL
);

-- Tablas de Limbic scoring
CREATE TABLE entity_access (
    entity_id    INTEGER PRIMARY KEY REFERENCES entities(id) ON DELETE CASCADE,
    access_count INTEGER NOT NULL DEFAULT 1,
    last_access  TEXT    NOT NULL DEFAULT (datetime('now'))
);

CREATE TABLE entity_access_log (
    entity_id    INTEGER NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
    access_date  TEXT    NOT NULL,
    access_count INTEGER NOT NULL DEFAULT 1,
    PRIMARY KEY (entity_id, access_date)
);

CREATE TABLE co_occurrences (
    entity_a_id  INTEGER NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
    entity_b_id  INTEGER NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
    co_count     INTEGER NOT NULL DEFAULT 1,
    last_co      TEXT    NOT NULL DEFAULT (datetime('now')),
    PRIMARY KEY (entity_a_id, entity_b_id)
);

-- Tablas de calidad de búsqueda
CREATE TABLE search_events (
    event_id     INTEGER PRIMARY KEY AUTOINCREMENT,
    query_text   TEXT    NOT NULL,
    query_hash   TEXT    NOT NULL,
    timestamp    TEXT    NOT NULL DEFAULT (datetime('now')),
    treatment    INTEGER NOT NULL,
    k_limit      INTEGER NOT NULL,
    num_results  INTEGER NOT NULL,
    duration_ms  REAL,
    engine_used  TEXT    NOT NULL
);

CREATE TABLE search_results (
    result_id     INTEGER PRIMARY KEY AUTOINCREMENT,
    event_id      INTEGER NOT NULL REFERENCES search_events(event_id),
    entity_id     INTEGER NOT NULL,
    entity_name   TEXT    NOT NULL,
    rank          INTEGER NOT NULL,
    limbic_score  REAL,
    cosine_sim    REAL,
    importance    REAL,
    temporal      REAL,
    cooc_boost    REAL,
    baseline_rank INTEGER,
    UNIQUE(event_id, entity_id)
);

CREATE TABLE implicit_feedback (
    feedback_id   INTEGER PRIMARY KEY AUTOINCREMENT,
    event_id      INTEGER NOT NULL REFERENCES search_events(event_id),
    entity_id     INTEGER NOT NULL,
    re_accessed   INTEGER NOT NULL DEFAULT 0,
    access_delta  INTEGER,
    session_id    TEXT
);

-- Tablas de reflexiones
CREATE TABLE reflections (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    target_type TEXT    NOT NULL,
    target_id   INTEGER,
    author      TEXT    NOT NULL DEFAULT 'sofia',
    content     TEXT    NOT NULL,
    mood        TEXT,
    created_at  TEXT    NOT NULL DEFAULT (datetime('now'))
);

-- Búsqueda de texto completo (FTS5)
CREATE VIRTUAL TABLE IF NOT EXISTS entity_fts
USING fts5(name, entity_type, obs_text, tokenize="unicode61");

CREATE VIRTUAL TABLE IF NOT EXISTS reflection_fts
USING fts5(content, tokenize="unicode61");

-- Embeddings vectoriales
CREATE VIRTUAL TABLE IF NOT EXISTS reflection_embeddings
USING vec0(embedding float[384] distance_metric=cosine);

-- Índices
CREATE INDEX idx_entities_name  ON entities(name);
CREATE INDEX idx_entities_type  ON entities(entity_type);
CREATE INDEX idx_obs_entity     ON observations(entity_id);
CREATE INDEX idx_obs_entity_superseded ON observations(entity_id, superseded_at);
CREATE INDEX idx_rel_from       ON relations(from_entity);
CREATE INDEX idx_rel_to         ON relations(to_entity);
CREATE INDEX idx_rel_type       ON relations(relation_type);
CREATE INDEX idx_access_last    ON entity_access(last_access);
CREATE INDEX idx_cooc_b         ON co_occurrences(entity_b_id);
CREATE INDEX idx_search_events_hash  ON search_events(query_hash);
CREATE INDEX idx_search_events_time  ON search_events(timestamp);
CREATE INDEX idx_search_results_event ON search_results(event_id);
CREATE INDEX idx_search_results_entity ON search_results(entity_id);
CREATE INDEX idx_implicit_event      ON implicit_feedback(event_id);

Índices

ÍndiceTablaColumna(s)Propósito
idx_entities_nameentitiesnameBúsqueda rápida por nombre de entidad
idx_entities_typeentitiesentity_typeFiltrar por tipo de entidad
idx_obs_entityobservationsentity_idRecuperar todas las observaciones de una entidad
idx_obs_entity_supersededobservationsentity_id, superseded_atFiltrar observaciones activas vs reemplazadas
idx_rel_fromrelationsfrom_entityRelaciones originadas desde una entidad
idx_rel_torelationsto_entityRelaciones dirigidas hacia una entidad
idx_rel_typerelationsrelation_typeFiltrar por tipo de relación
idx_access_lastentity_accesslast_accessOrdenar por recencia de acceso (decaimiento temporal)
idx_cooc_bco_occurrencesentity_b_idBuscar co-ocurrencias por entidad B
idx_search_events_hashsearch_eventsquery_hashDeduplicar consultas idénticas
idx_search_events_timesearch_eventstimestampAnálisis de calidad de búsqueda por rango temporal
idx_search_results_eventsearch_resultsevent_idRecuperar todos los resultados de un evento
idx_search_results_entitysearch_resultsentity_idHistorial de ranking de una entidad específica
idx_implicit_eventimplicit_feedbackevent_idRecuperar feedback de un evento de búsqueda

Modelos Pydantic

Los modelos Pydantic cumplen un doble propósito en MCP Memory v2:

  1. Validación de entrada: Cada tool MCP recibe JSON del cliente. Los modelos validan estructura y tipos antes de tocar la base de datos.
  2. Serialización de salida: Las respuestas de las tools se serializan a JSON con tipos consistentes.

Las 19 tools MCP usan estos modelos para validación y serialización.

Código fuente completo (models.py)

from pydantic import BaseModel, Field


class EntityInput(BaseModel):
    name: str = Field(..., min_length=1)
    entityType: str = Field(default="Generic")
    observations: list[str] = Field(default_factory=list)
    status: str = Field(default="activo")


class RelationInput(BaseModel):
    from_entity: str = Field(..., alias="from")
    to_entity: str   = Field(..., alias="to")
    relationType: str
    context: str | None = None
    model_config = {"populate_by_name": True}

EntityInput

Modelo de entrada para crear o actualizar entidades.

CampoTipoObligatorioPor defectoValidación
namestrmin_length=1
entityTypestrNo"Generic"
observationslist[str]No[] (vía factory)Lista nueva por instancia
statusstrNo"activo"

:::note[default_factory vs default=[]] observations usa default_factory=list en lugar de default=[] para evitar el clásico bug de Python con argumentos por defecto mutables. Con default=[], todas las instancias compartirían el mismo objeto lista; con default_factory, cada instancia recibe una lista nueva. :::

RelationInput

Modelo de entrada para crear relaciones. RelationInput valida los datos entrantes del cliente.

CampoAlias JSONTipoObligatorioDescripción
from_entity"from"strNombre de la entidad origen
to_entity"to"strNombre de la entidad destino
relationTypestrTipo de relación
contextstrNoContexto narrativo opcional del enlace

:::note[Diseño de alias] from y to son palabras reservadas de Python, por lo que no pueden usarse como nombres de atributos. Los alias de Pydantic resuelven esto:

  • En Python: se accede como relation.from_entity (nombre legal)
  • En JSON: se serializa como "from" (formato esperado por Anthropic MCP)

populate_by_name=True habilita deserialización bidireccional — ambas formas son aceptadas:

# Ambas funcionan:
RelationInput(**{"from": "EntityA", "to": "EntityB", "relationType": "uses"})
RelationInput(**{"from_entity": "EntityA", "to_entity": "EntityB", "relationType": "uses"})

Esto garantiza compatibilidad con el formato MCP de Anthropic (que envía "from"/"to") y con llamadas internas que usan nombres de atributos Python. :::

Configuración PRAGMA de SQLite

Los siguientes PRAGMAs se establecen en cada conexión a la base de datos en MemoryStore:

PRAGMA journal_mode = WAL       # Escritura sin bloquear lecturas
PRAGMA busy_timeout  = 10000    # Esperar 10s si está bloqueado
PRAGMA synchronous   = NORMAL   # Balance entre seguridad y velocidad
PRAGMA cache_size    = -64000   # 64 MB de caché
PRAGMA temp_store    = MEMORY   # Tablas temporales en RAM
PRAGMA foreign_keys  = ON       # Aplicar integridad referencial
PRAGMAValorJustificación
journal_modeWALWrite-Ahead Logging permite lectores concurrentes mientras un escritor está activo. Los lectores nunca bloquean escritores y viceversa.
busy_timeout10000Espera hasta 10 segundos por contención de bloqueo antes de lanzar SQLITE_BUSY. En el contexto MCP (llamadas secuenciales a tools), esto es más que suficiente.
synchronousNORMALSuficientemente seguro para modo WAL (el archivo WAL sigue sincronizándose), pero más rápido que FULL que también sincroniza el archivo de base de datos. El balance adecuado para un knowledge graph local.
cache_size-64000Caché de páginas de 64 MB. Valores negativos indican KiB. Reduce I/O de disco para consultas repetidas.
temp_storeMEMORYLas tablas temporales y resultados intermedios permanecen en RAM. Acelera consultas complejas y reconstrucción de índices.
foreign_keysONAplica las restricciones ON DELETE CASCADE. Sin este pragma, SQLite ignora silenciosamente la aplicación de claves foráneas.

Modo WAL y concurrencia

OperaciónComportamiento
Lecturas concurrentesPermitidas (WAL soporta múltiples lectores simultáneos)
EscriturasSecuenciales (un único escritor)
Contención de bloqueoLos lectores esperan hasta 10 segundos (busy_timeout) por un bloqueo de escritura
Caché64 MB en memoria para reducir I/O

En el contexto MCP, donde las llamadas a tools son secuenciales, este modelo es adecuado.

Gotchas técnicos

vec0 no soporta CASCADE

Las tablas virtuales vec0 no participan en ON DELETE CASCADE. Al eliminar una entidad, las observaciones y relaciones se eliminan por CASCADE, pero el embedding no. Siempre elimina los embeddings manualmente antes de la fila de la entidad (ver el ejemplo de código en la sección entity_embeddings más arriba).

Error “Cannot Start a Transaction”

sqlite-vec puede fallar intermitentemente con "cannot start a transaction" durante operaciones de eliminación. Este es un bug conocido en sqlite-vec relacionado con cómo las tablas virtuales interactúan con el sistema de transacciones de SQLite. Reintentar la operación suele resolverlo. El código base captura este error y lo registra como warning sin fallar.

Los embeddings no son incrementales

Cada vez que las observaciones de una entidad cambian, el embedding se regenera completamente desde cero. El texto de entrada incluye una instantánea completa de todas las observaciones actuales. Esto significa:

  • Consistencia: el embedding siempre refleja el estado actual, sin artefactos de actualizaciones parciales
  • Coste: cada actualización dispara una codificación ONNX completa (~5 ms en CPU para un vector individual)
  • Sobrescritura: INSERT OR REPLACE en vec0 asegura que versiones viejas no se acumulen

Motor de embeddings diferido (lazy)

El servidor MCP arranca en ~1 segundo porque no carga el modelo de embeddings al iniciar. La arquitectura diferida tiene dos capas:

  1. Import diferido: mcp_memory.embeddings no se importa en el ámbito del módulo en server.py. La importación ocurre dentro de _get_engine().
  2. Instancia diferida: EmbeddingEngine.get_instance() crea el singleton solo en la primera llamada.

Consecuencias:

  • Primera llamada a search_semantic: ~3–5 segundos adicionales mientras el modelo carga
  • Llamadas posteriores: respuestas en milisegundos (el motor ya está en memoria)
  • Arranque del servidor: siempre rápido, independientemente de si el modelo está descargado