Referencia de API
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
Sección titulada «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
Sección titulada «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{ co_occurrences : "co-occurs" entities ||--|| entity_fts : "indexed (FTS5)"
entities { INTEGER id PK TEXT name UK TEXT entity_type TEXT created_at TEXT updated_at }
observations { INTEGER id PK INTEGER entity_id FK TEXT content TEXT created_at }
relations { INTEGER id PK INTEGER from_entity FK INTEGER to_entity FK TEXT relation_type TEXT created_at }
entity_embeddings { INTEGER rowid PK FLOAT embedding_384 }
entity_access { INTEGER entity_id PK_FK INTEGER access_count TEXT last_access }
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 }
db_metadata { TEXT key PK TEXT value }Esquema de base de datos
Sección titulada «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.
Dos tablas auxiliares adicionales (entity_access, co_occurrences) alimentan el sistema de Limbic Scoring, y una tabla virtual FTS5 habilita la búsqueda de texto completo.
entities
Sección titulada «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) |
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
Sección titulada «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 |
created_at | TEXT | NOT NULL DEFAULT (datetime('now')) | Timestamp de creación |
relations
Sección titulada «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) |
created_at | TEXT | NOT NULL DEFAULT (datetime('now')) | Timestamp de creación |
entity_embeddings (Virtual)
Sección titulada «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 |
El enlace basado en rowid permite JOINs directos sin una columna FK explícita:
SELECT e.name, e.entity_typeFROM entities eJOIN entity_embeddings ee ON e.id = ee.rowidWHERE ee.embedding MATCH ?ORDER BY distance;db_metadata
Sección titulada «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
Sección titulada «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
Sección titulada «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 |
entity_fts (FTS5 Virtual)
Sección titulada «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.
Esquema SQL completo
Sección titulada «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', 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, 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, created_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(from_entity, to_entity, relation_type));
CREATE VIRTUAL TABLE entity_embeddingsUSING vec0(embedding float[384] distance_metric=cosine);
CREATE TABLE db_metadata ( key TEXT PRIMARY KEY, value TEXT NOT NULL);
-- Tablas de Limbic scoringCREATE 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 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));
-- Búsqueda de texto completo (FTS5)CREATE VIRTUAL TABLE IF NOT EXISTS entity_ftsUSING fts5(name, entity_type, obs_text, tokenize="unicode61");
-- ÍndicesCREATE 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_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);Índices
Sección titulada «Í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_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 |
Modelos Pydantic
Sección titulada «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 10 tools MCP usan estos modelos para validar y retornar datos sobre entidades y relaciones.
Código fuente completo (models.py)
Sección titulada «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)
class EntityOutput(BaseModel): name: str entityType: str observations: list[str]
class RelationInput(BaseModel): from_entity: str = Field(..., alias="from") to_entity: str = Field(..., alias="to") relationType: str model_config = {"populate_by_name": True}
class RelationOutput(BaseModel): from_entity: str = Field(..., alias="from") to_entity: str = Field(..., alias="to") relationType: str model_config = {"populate_by_name": True}EntityInput
Sección titulada «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 |
EntityOutput
Sección titulada «EntityOutput»Modelo de salida para respuestas de entidades. Todos los campos son obligatorios — el servidor siempre los rellena desde la base de datos.
RelationInput / RelationOutput
Sección titulada «RelationInput / RelationOutput»Ambos modelos comparten la misma estructura de campos. RelationInput valida los datos entrantes del cliente; RelationOutput serializa las respuestas.
| Campo | Alias JSON | Tipo | Obligatorio |
|---|---|---|---|
from_entity | "from" | str | Sí |
to_entity | "to" | str | Sí |
relationType | — | str | Sí |
Configuración PRAGMA de SQLite
Sección titulada «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 lecturasPRAGMA busy_timeout = 10000 # Esperar 10s si está bloqueadoPRAGMA synchronous = NORMAL # Balance entre seguridad y velocidadPRAGMA cache_size = -64000 # 64 MB de cachéPRAGMA temp_store = MEMORY # Tablas temporales en RAMPRAGMA 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
Sección titulada «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
Sección titulada «Gotchas técnicos»vec0 no soporta CASCADE
Sección titulada «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”
Sección titulada «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
Sección titulada «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)
Sección titulada «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