Ir al contenido

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.

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.

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
}

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.

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

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
created_atTEXTNOT NULL DEFAULT (datetime('now'))Timestamp de creación

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)
created_atTEXTNOT NULL DEFAULT (datetime('now'))Timestamp de creación

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

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;

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

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)

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

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.

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_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 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_fts
USING fts5(name, entity_type, obs_text, tokenize="unicode61");
-- Í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_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);
Í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_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

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 10 tools MCP usan estos modelos para validar y retornar datos sobre entidades y relaciones.

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}

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

Modelo de salida para respuestas de entidades. Todos los campos son obligatorios — el servidor siempre los rellena desde la base de datos.

Ambos modelos comparten la misma estructura de campos. RelationInput valida los datos entrantes del cliente; RelationOutput serializa las respuestas.

CampoAlias JSONTipoObligatorio
from_entity"from"str
to_entity"to"str
relationTypestr

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

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

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.

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

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