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ámetro | Valor |
|---|---|
| Entry point | mcp-memory → mcp_memory.server:main |
| Transporte | stdio |
| Logs | stderr |
| 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.
| Columna | Tipo | Restricciones | Descripción |
|---|---|---|---|
id | INTEGER | PRIMARY KEY AUTOINCREMENT | Identificador interno único |
name | TEXT | NOT NULL UNIQUE | Nombre legible de la entidad. Clave de negocio — dos entidades no pueden compartir nombre |
entity_type | TEXT | NOT NULL DEFAULT 'Generic' | Clasificación de la entidad (ej. Sesion, Componente, Sistema) |
status | TEXT | NOT NULL DEFAULT 'activo' | Estado del ciclo de vida de la entidad (ej. activo, archivado) |
created_at | TEXT | NOT NULL DEFAULT (datetime('now')) | Timestamp de creación en formato ISO-8601 |
updated_at | TEXT | NOT 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.
| Columna | Tipo | Restricciones | Descripción |
|---|---|---|---|
id | INTEGER | PRIMARY KEY AUTOINCREMENT | Identificador interno único |
entity_id | INTEGER | NOT NULL REFERENCES entities(id) ON DELETE CASCADE | FK a la entidad padre. La eliminación en cascada borra las observaciones al eliminar la entidad |
content | TEXT | NOT NULL | Contenido de texto libre de la observación |
kind | TEXT | NOT NULL DEFAULT 'generic' | Categoría de la observación (ej. generic, milestone, risk) |
supersedes | INTEGER | REFERENCES observations(id) | FK a la observación que esta reemplaza |
superseded_at | TEXT | Timestamp en que esta observación fue marcada como reemplazada | |
similarity_flag | INTEGER | NOT NULL DEFAULT 0 | Flag para flujos de detección de duplicados |
created_at | TEXT | NOT 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.
| Columna | Tipo | Restricciones | Descripción |
|---|---|---|---|
id | INTEGER | PRIMARY KEY AUTOINCREMENT | Identificador interno único |
from_entity | INTEGER | NOT NULL REFERENCES entities(id) ON DELETE CASCADE | FK a la entidad origen |
to_entity | INTEGER | NOT NULL REFERENCES entities(id) ON DELETE CASCADE | FK a la entidad destino |
relation_type | TEXT | NOT NULL | Tipo de relación (ej. uses, depends_on, part_of) |
context | TEXT | Contexto narrativo opcional de la relación | |
active | INTEGER | NOT NULL DEFAULT 1 | Flag de borrado lógico (1 = activa, 0 = finalizada) |
ended_at | TEXT | Timestamp en que la relación fue marcada como finalizada | |
created_at | TEXT | NOT 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.
| Columna | Tipo | Descripción |
|---|---|---|
embedding | float[384] | Vector de 384 dimensiones generado por el modelo ONNX. Métrica de distancia: coseno |
rowid | INTEGER (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).
| Columna | Tipo | Restricciones | Descripción |
|---|---|---|---|
key | TEXT | PRIMARY KEY | Clave única del metadato |
value | TEXT | NOT NULL | Valor 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.
| Columna | Tipo | Restricciones | Descripción |
|---|---|---|---|
entity_id | INTEGER | PRIMARY KEY REFERENCES entities(id) ON DELETE CASCADE | FK a la entidad. Una fila por entidad |
access_count | INTEGER | NOT NULL DEFAULT 1 | Número de veces que la entidad apareció en resultados de búsqueda semántica |
last_access | TEXT | NOT 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.
| Columna | Tipo | Restricciones | Descripción |
|---|---|---|---|
entity_a_id | INTEGER | NOT NULL REFERENCES entities(id) ON DELETE CASCADE | FK a la entidad con el ID inferior (orden canónico) |
entity_b_id | INTEGER | NOT NULL REFERENCES entities(id) ON DELETE CASCADE | FK a la entidad con el ID superior |
co_count | INTEGER | NOT NULL DEFAULT 1 | Número de co-ocurrencias registradas |
last_co | TEXT | NOT 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.
| Columna | Tipo | Descripción |
|---|---|---|
name | TEXT | Nombre de la entidad (buscable) |
entity_type | TEXT | Tipo de entidad (buscable) |
obs_text | TEXT | Todas las observaciones concatenadas con " | " como separador |
rowid | INTEGER (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.
| Columna | Tipo | Restricciones | Descripción |
|---|---|---|---|
entity_id | INTEGER | NOT NULL REFERENCES entities(id) ON DELETE CASCADE | FK a la entidad |
access_date | TEXT | NOT NULL | Fecha de acceso (ISO-8601) |
access_count | INTEGER | NOT NULL DEFAULT 1 | Nú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.
| Columna | Tipo | Restricciones | Descripción |
|---|---|---|---|
event_id | INTEGER | PRIMARY KEY AUTOINCREMENT | Identificador interno único |
query_text | TEXT | NOT NULL | Cadena de consulta original |
query_hash | TEXT | NOT NULL | Hash de la consulta para deduplicación |
timestamp | TEXT | NOT NULL DEFAULT (datetime('now')) | Timestamp de ejecución de la consulta |
treatment | INTEGER | NOT NULL | Identificador del brazo del test A/B |
k_limit | INTEGER | NOT NULL | Máximo de resultados solicitados |
num_results | INTEGER | NOT NULL | Número real de resultados devueltos |
duration_ms | REAL | Latencia de la consulta en milisegundos | |
engine_used | TEXT | NOT NULL | Variante del motor de búsqueda usado |
search_results
Datos de ranking por entidad producidos por cada llamada a search_semantic.
| Columna | Tipo | Restricciones | Descripción |
|---|---|---|---|
result_id | INTEGER | PRIMARY KEY AUTOINCREMENT | Identificador interno único |
event_id | INTEGER | NOT NULL REFERENCES search_events(event_id) | FK al evento de búsqueda padre |
entity_id | INTEGER | NOT NULL | FK a la entidad rankeada |
entity_name | TEXT | NOT NULL | Snapshot del nombre de la entidad en el momento |
rank | INTEGER | NOT NULL | Posición en la lista de resultados (base 1) |
limbic_score | REAL | Puntaje final combinado | |
cosine_sim | REAL | Similitud coseno raw | |
importance | REAL | Componente de conteo de accesos | |
temporal | REAL | Componente de decaimiento temporal | |
cooc_boost | REAL | Boost de co-ocurrencia | |
baseline_rank | INTEGER | Rank 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.
| Columna | Tipo | Restricciones | Descripción |
|---|---|---|---|
feedback_id | INTEGER | PRIMARY KEY AUTOINCREMENT | Identificador interno único |
event_id | INTEGER | NOT NULL REFERENCES search_events(event_id) | FK al evento de búsqueda original |
entity_id | INTEGER | NOT NULL | FK a la entidad re-accedida |
re_accessed | INTEGER | NOT NULL DEFAULT 0 | Si la entidad fue re-accedida (1 = sí) |
access_delta | INTEGER | Segundos entre búsqueda y re-acceso | |
session_id | TEXT | Identificador de sesión opcional |
reflections
Reflexiones narrativas que dan contexto y significado a los recuerdos.
| Columna | Tipo | Restricciones | Descripción |
|---|---|---|---|
id | INTEGER | PRIMARY KEY AUTOINCREMENT | Identificador interno único |
target_type | TEXT | NOT NULL | A qué apunta esta reflexión (entity, session, relation, global) |
target_id | INTEGER | ID de la entidad/relación objetivo (NULL para global) | |
author | TEXT | NOT NULL DEFAULT 'sofia' | Autor de la reflexión (nolan o sofia) |
content | TEXT | NOT NULL | Cuerpo de texto libre de la reflexión |
mood | TEXT | Tag de estado de ánimo opcional (frustracion, satisfaccion, etc.) | |
created_at | TEXT | NOT 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.
| Columna | Tipo | Descripción |
|---|---|---|
content | TEXT | Texto de la reflexión (buscable) |
rowid | INTEGER (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.
| Columna | Tipo | Descripción |
|---|---|---|
embedding | float[384] | Vector de 384 dimensiones. Métrica de distancia: coseno |
rowid | INTEGER (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
| Índice | Tabla | Columna(s) | Propósito |
|---|---|---|---|
idx_entities_name | entities | name | Búsqueda rápida por nombre de entidad |
idx_entities_type | entities | entity_type | Filtrar por tipo de entidad |
idx_obs_entity | observations | entity_id | Recuperar todas las observaciones de una entidad |
idx_obs_entity_superseded | observations | entity_id, superseded_at | Filtrar observaciones activas vs reemplazadas |
idx_rel_from | relations | from_entity | Relaciones originadas desde una entidad |
idx_rel_to | relations | to_entity | Relaciones dirigidas hacia una entidad |
idx_rel_type | relations | relation_type | Filtrar por tipo de relación |
idx_access_last | entity_access | last_access | Ordenar por recencia de acceso (decaimiento temporal) |
idx_cooc_b | co_occurrences | entity_b_id | Buscar co-ocurrencias por entidad B |
idx_search_events_hash | search_events | query_hash | Deduplicar consultas idénticas |
idx_search_events_time | search_events | timestamp | Análisis de calidad de búsqueda por rango temporal |
idx_search_results_event | search_results | event_id | Recuperar todos los resultados de un evento |
idx_search_results_entity | search_results | entity_id | Historial de ranking de una entidad específica |
idx_implicit_event | implicit_feedback | event_id | Recuperar feedback de un evento de búsqueda |
Modelos Pydantic
Los modelos Pydantic cumplen un doble propósito en MCP Memory v2:
- Validación de entrada: Cada tool MCP recibe JSON del cliente. Los modelos validan estructura y tipos antes de tocar la base de datos.
- 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.
| Campo | Tipo | Obligatorio | Por defecto | Validación |
|---|---|---|---|---|
name | str | Sí | — | min_length=1 |
entityType | str | No | "Generic" | — |
observations | list[str] | No | [] (vía factory) | Lista nueva por instancia |
status | str | No | "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.
| Campo | Alias JSON | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
from_entity | "from" | str | Sí | Nombre de la entidad origen |
to_entity | "to" | str | Sí | Nombre de la entidad destino |
relationType | — | str | Sí | Tipo de relación |
context | — | str | No | Contexto 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
| PRAGMA | Valor | Justificación |
|---|---|---|
journal_mode | WAL | Write-Ahead Logging permite lectores concurrentes mientras un escritor está activo. Los lectores nunca bloquean escritores y viceversa. |
busy_timeout | 10000 | Espera 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. |
synchronous | NORMAL | Suficientemente 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 | -64000 | Caché de páginas de 64 MB. Valores negativos indican KiB. Reduce I/O de disco para consultas repetidas. |
temp_store | MEMORY | Las tablas temporales y resultados intermedios permanecen en RAM. Acelera consultas complejas y reconstrucción de índices. |
foreign_keys | ON | Aplica las restricciones ON DELETE CASCADE. Sin este pragma, SQLite ignora silenciosamente la aplicación de claves foráneas. |
Modo WAL y concurrencia
| Operación | Comportamiento |
|---|---|
| Lecturas concurrentes | Permitidas (WAL soporta múltiples lectores simultáneos) |
| Escrituras | Secuenciales (un único escritor) |
| Contención de bloqueo | Los 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 REPLACEen 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:
- Import diferido:
mcp_memory.embeddingsno se importa en el ámbito del módulo enserver.py. La importación ocurre dentro de_get_engine(). - 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