mcp-memorybank
Semillas, gobierna y navega una base de conocimiento memory_bank: escrituras controladas, enrutamiento sin lectura y deriva contra el código.
Documentación
mcp-memorybank
Un servidor MCP para una base de conocimiento memory_bank. Se siembra desde cero, controla cada escritura
contra las reglas que el propio banco declara, enruta al documento correcto sin leer el banco,
y reporta dónde el código ha avanzado sin que los documentos lo reflejen.
No escribe nada por iniciativa propia. Cada documento llega desde una llamada deliberada que lleva un propósito, una dependencia ascendente y una entrada de índice; lo que el hook de fin de sesión recopila termina en una cuarentena que solo un humano vacía.
Especificación — specification.md, plan — implementation-plan.md, de qué está hecho un banco sembrado y dónde se tensa — architecture.md.
Todo en el plan está hecho: E0 índice, E1 lecturas, E2 validación, E3 grafo, E4 búsqueda y delta, E5 escrituras.
En qué es mediblemente bueno, y en qué no
Contado en seis bancos en uso real, analizando las sesiones en lugar de por impresión:
writing into the bank, through the server 985 calls
reading the bank, through the server 187 calls
reading the bank, around the server 1755 calls (cat, grep, Read)
bank_route specifically 33 calls
La mitad de escritura funciona. bank_create, bank_edit y bank_update_section se usan en cada
banco, con o sin instrucción de hacerlo, porque escribir un documento gobernado a mano es
más difícil que llamar a la herramienta: tendría que reproducir el frontmatter, resolver el
derived_from, y registrarse en un índice de sección, y fallaría la validación si se equivocara en
cualquiera de eso. El noventa por ciento de todas las llamadas al servidor son escrituras.
La mitad de lectura está mayormente evadida. Nueve de cada diez veces un agente abre el banco con cat
y grep en lugar de preguntar a bank_route qué documentos importan. Esto no es una afirmación de que el enrutamiento
responda mal — nadie ha medido eso. Es una afirmación de que no se le pregunta, porque alcanzar
la shell es lo que un modelo hace en cada repositorio y nada aquí supera el hábito.
Dos cosas se siguen, y ninguna es cómoda. El enrutamiento es la característica con la que este README abre y la sobre la que se construye la aritmética de tokens abajo, y es la menos usada. Editar documentos existentes fue escrito en la especificación como algo que el servidor no haría — ver §8 — y resultó ser la mitad que carga el proyecto.
La única palanca con evidencia detrás es el párrafo bajo Diciéndole al agente que lo use: los cuatro bancos que lo llevan enrutan ocho veces más a menudo que los dos que no, 32% de lecturas contra 4%. Eso es una correlación en seis proyectos, y se está probando adecuadamente — ver A-06 en backlog.md.
Cómo funciona
No hay base de datos, ni daemon, ni configuración. Un directorio de archivos markdown es todo el estado, y el servidor es un lector de eso que resulta hablar MCP. Todo lo que sabe lo re-deriva de los archivos; elimina su proceso y nada se pierde, porque nada se guardó nunca en otro lugar.
El índice, y por qué no hay watcher
Al iniciar, el servidor recorre el banco una vez, y en cada llamada lo recorre de nuevo: readdir, stat,
y un parse solo para los archivos cuyo mtime se movió. Una construcción en frío de un banco de 368 documentos toma alrededor de
85 ms; el recorrido antes de una llamada sin cambios cuesta 11–13 ms.
Por eso no hay file watcher. Un watcher ahorraría esos milisegundos y compraría una clase de error a cambio — un índice que se ha divergido silenciosamente del disco, en una herramienta cuyo trabajo entero es ser confiable sobre lo que el disco dice. La verificación barata gana en ambos sentidos.
En qué se convierte un documento
Cada archivo se parsea una vez en un registro: los campos de frontmatter, los encabezados de sección de segundo nivel
con sus rangos de línea, tamaño en bytes, mtime — y una cosa que el archivo no contiene, una capa
calculada desde la ruta (dna, knowledge, decision, delivery, flow, inbox, other).
El frontmatter pasa por gray-matter en exactamente un lugar. Cuando el YAML es inválido — un
colon sin comillas en purpose es el caso común — el documento no cae fuera del índice: su metadata se
recupera línea por línea y el error de parse se guarda para que bank_validate lo reporte. Un documento que
desaparece del enrutamiento por un error tipográfico es peor que uno que rankea mal.
El contrato viene del banco
doc_kind, doc_function, status, si derived_from es obligatorio, qué documento es la
raíz declarada — todo se lee del propio dna/frontmatter.md y dna/governance.md del banco
en el momento de refresco. Nada está hardcodeado, y los conjuntos son abiertos.
Esto no es cortesía: los bancos no están de acuerdo sobre su propio vocabulario, y un enum fijo rechaza una
minoría considerable de documentos reales solo por doc_kind. Un banco sin dna/ aún funciona: el servidor corre en modo degradado — lectura,
enrutamiento, búsqueda y las reglas estructurales — y lo dice en lugar de imponer un contrato que nadie
declaró.
Enrutamiento: rankear el encabezado, nunca la prosa
bank_route responde "qué debería leer sobre esto", y lee solo lo que un humano escribió a mano
sobre cada documento — nunca el cuerpo. Cuatro campos, con pesos fijos:
| Campo | Peso | Puntuado como |
|---|---|---|
canonical_for | 5 | cuánto de una clave de hecho cubre la pregunta, no si una palabra de ella coincidió |
purpose | 3 | solapamiento de palabras |
title | 2 | solapamiento de palabras |
| encabezados de sección | 1 | el encabezado que mejor coincide, que también se devuelve para que la respuesta se lea con ámbito de sección |
La puntuación cruda se multiplica luego, y los multiplicadores son donde el ranking realmente obtiene su juicio:
- Capa.
knowledge×1.5,decision×1.2,dna/flow/other×1.0,delivery×0.6,inbox×0.2. La entrega se amortigua porque en un banco que ha estado en uso por un tiempo es la mayoría de los documentos; sin esto, una pregunta sobre una regla devuelve las características cerradas que la mencionan. - Estado.
active×1,draft×0.7,archived×0.2. - Trabajo cerrado.
delivery_status: doneocancelledreduce la puntuación a la mitad de nuevo. Una característica terminada es historia, no una respuesta. - Intención. Una pregunta que contiene por qué, razón, en lugar de, почему, вместо re-pondera
toda la ejecución hacia decisiones:
decision×1.6,knowledge×1.2. "Por qué X" y "qué es X" son preguntas diferentes y no deberían devolver el mismo documento primero.
Las plantillas nunca aparecen en los resultados: son estructuralmente idénticas a los documentos reales y inundarían cada lista.
Búsqueda: un índice diferente para una pregunta diferente
bank_search no es un respaldo para el enrutamiento, responde la otra mitad. El enrutamiento rankea el
encabezado escrito a mano ("qué documento es sobre esto"); la búsqueda lee la prosa ("dónde aparece
esta cadena realmente"). Identificadores, mensajes de error y literales viven solo en los cuerpos.
Construye un índice invertido sobre los cuerpos de los documentos, incrementalmente en la misma verificación de mtime. Una consulta
coincide en tres niveles por confianza — la palabra tal como se escribió pesa 10, su equivalente en el otro
idioma 6, un fragmento de un token compuesto 1 — y los documentos que coinciden con cada concepto se rankean
antes que los documentos que coinciden con algunos. El nivel de fragmento es lo que mantiene a FT-042 de elevar el
registro de features/README.md, con sus setenta líneas de FT-*, por encima de la característica misma.
Escritura: una operación, o ninguna
bank_create escribe el documento, llena el frontmatter que el contrato pide, y lo registra en
el índice de sección en la misma llamada — así el paso de registro no puede olvidarse, que es la
forma más común en que un banco se pudre. El registro copia la forma de la última entrada en ese índice,
fila de tabla o viñeta o elemento numerado, así un archivo escrito a mano no se reformatea.
Antes de todo eso, se niega, con la razón nombrada: la ruta está ocupada, derived_from no
resuelve, canonical_for ya es propiedad de otro documento, la ruta sale de la raíz del banco, o
no termina en .md. Una negativa no escribe nada en absoluto — sin archivo parcial, sin línea de índice huérfana.
Dos reglas se siguen de esos portones, y son la razón por la que las escrituras valen la pena: un conflicto SSoT y un borde roto no pueden entrar al banco a través de este servidor. Solo pueden llegar editando un archivo a sus espaldas.
El grafo
bank_graph recorre derived_from en anchura con un techo de nodos, así un documento hub no arrastra
todo el banco y un ciclo no se repite. Cada borde cae en uno de tres resultados: interno (un
nodo), external (sale de la raíz del banco — en un monorepo, los bancos se anidan), o broken. up es sobre lo que un
documento se construye; down es el radio de explosión de cambiarlo.
Un borde externo se sigue exactamente un salto: el frontmatter del objetivo se lee y se devuelve bajo
neighbours, y nada más sobre él. No se indexa, no se valida, no se busca, y sus propios bordes no se
recorren. El límite se queda donde estaba — pero en un monorepo esos bordes llevan decisiones
reales, y un radio de explosión que termina silenciosamente en la pared del repositorio es incorrecto en lugar de parcial.
Pasa neighbours: false para saltarte las lecturas.
Deriva
bank_drift responde la pregunta que la validación no puede: no "falta algo" sino "lo que
escribimos se ha vuelto obsoleto". Un documento que describe código lo lista en anchors:, y el servidor compara el
último commit que tocó el documento contra el último commit que tocó el código. Donde el código está
por delante por más del umbral, lo dice — y donde un ancla apunta a una ruta que ya no
existe, siempre lo dice, porque eso es el código moviéndose por debajo del documento.
Nunca adivina qué documento es dueño de qué archivo. El emparejamiento está anotado a mano o no existe, lo que significa que un banco que no ha sido anotado obtiene una respuesta vacía honesta y una nota diciendo por qué. Y reporta sin editar: si una brecha de seis meses importa no es un juicio que una marca de tiempo pueda hacer.
Qué contiene un banco sembrado
bank_init escribe 24 archivos: el conjunto de gobernanza en dna/, un índice raíz, ocho registros de sección,
los cuatro documentos de flujo, un puntero donde estarían las plantillas, y dos borradores para llenar. Tres secciones más — epics, prd, prompts — se construyen
la primera vez que un documento necesita una, índice y entrada de índice raíz incluidos, en lugar de estar vacías
desde el inicio.
Las plantillas de documento no se copian. Viajan con el servidor y bank_create las lee desde
allí, así un banco se comporta exactamente como si las tuviera; --materialize-flows las copia para un
proyecto que pretende cambiar una, y desde entonces las copias del propio banco ganan. Los flujos de prosa sí se
copian, porque la gente los lee y el enrutamiento responde con ellos.
dna/ se copia también, y ese no es negociable: es la ley por la que el banco se juzga, y el
diseño descansa en que viaje con el corpus. La capa de cada directorio de nivel superior y su peso de ranking
se declaran allí también — un banco que renombra una sección lo dice en esa tabla y mantiene su
peso, y un banco que no declara nada obtiene el mapa integrado.
Qué no hará
No crea un documento editando — cada documento nuevo pasa por bank_create, así nada
entra al banco sin una verificación de ruta, un derived_from que resuelve y una entrada de índice. No
promueve nada fuera de _inbox/ por su cuenta. No inventa un esquema, y no impone una
regla que el banco no haya declarado. No elimina un documento fuera de _inbox/. Y no
indexa, valida ni busca nada fuera de la raíz del banco — el único salto que bank_graph da hacia un
banco vecino lee un encabezado y se detiene allí.
Qué cuesta en tokens
Conectar un servidor no es gratuito, y el costo se paga en dos monedas diferentes. Todas las cifras a continuación se midieron en la compilación enviada, contando caracteres de las cargas útiles JSON-RPC reales y convirtiendo a 3.5 caracteres por token — el JSON denso se acerca más a eso que los 4 que se adaptan a la prosa.
La mitad fija se paga en cada solicitud, ya sea que se llame a una herramienta o no, porque las definiciones viven en el contexto:
| caracteres | ~tokens | |
|---|---|---|
| instrucciones del servidor | 343 | 98 |
| 15 definiciones de herramientas | 20,473 | 5,849 |
| 5 indicaciones | 1,357 | 388 |
| 4 recursos | 913 | 261 |
| total, por solicitud | 23,086 | ~6,600 |
bank_create es la definición más costosa con ~715 tokens — siete parámetros y una descripción
que tiene que explicar la puerta. bank_changed es la más barata con ~183.
Esa tabla es lo que paga un proyecto con un banco. Un proyecto que no tiene ninguno recibe solo bank_init
y paga ~450; uno desactivado — por --off o un archivo .memorybank-off — paga ~25 por una superficie vacía. Ver
Instalándolo una vez, para cada proyecto — la razón por la que el
servidor se molesta en hacer esa distinción es que una instalación global de otro modo cobraría la tarifa
completa en cada proyecto de la máquina, incluido cada proyecto que nunca tendrá un banco.
La mitad variable es donde recupera la mitad fija. En un banco de 27 documentos:
| caracteres | ~tokens | |
|---|---|---|
| leer todo el banco | 91,319 | 26,091 |
una respuesta bank_route, 3 resultados | 839 | 240 |
| los tres documentos que nombró | 8,386 | 2,396 |
Enrutar respuestas en 240 tokens lo que leer el corpus cuesta 26,091 — y una décima parte de lo que cuesta leer
incluso los tres documentos correctos, porque devuelve rutas, títulos, purpose y una línea de
razonamiento en lugar de cualquier prosa. Qué leer después es entonces una decisión tomada con evidencia.
bank_read está limitado por separado: 40,000 bytes por lote, aproximadamente 11,000 tokens. Pide veinte
documentos y obtienes lo que cabe más una lista de lo que el presupuesto no alcanzó, desglosado por
sección, para que una sola llamada no pueda inundar el contexto.
Dónde está el punto de equilibrio. Los ~6,600 se recuperan la primera vez que el servidor evita una lectura innecesaria, y eso sucede de inmediato en cualquier banco lo suficientemente grande como para importar. Medido en cuatro bancos de la misma línea:
27 documents ~26k tokens to read whole
78 documents ~87k
96 documents ~158k
112 documents ~305k
En la parte superior de ese rango, el corpus son dos ventanas de contexto completas y leerlo no es una opción a ningún precio, mientras que una respuesta enrutada se mantiene en los cientos bajos de tokens.
La conclusión honesta es que en un banco pequeño el servidor pierde. Mientras el corpus aún cabe en una
ventana de contexto, cat es más barato que un cargo fijo de 6,600 tokens, y la prueba de campo midió
exactamente eso: en un banco lo suficientemente pequeño para leerlo completo, el enrutamiento compite con "ya en contexto" y
pierde. El servidor gana su costo en corpus que han superado ser leídos.
Y los tokens no son lo principal que se compra. La mitad de las herramientas no ahorran nada en absoluto — bank_create,
bank_validate y bank_drift solo gastan. Existen para que el banco mantenga su forma, lo que ninguna cantidad
de presupuesto de contexto hará por sí sola. El ahorro en el enrutamiento es un efecto secundario, no el punto.
Ejecutándolo
Conectarlo a un proyecto — <project>/.mcp.json:
{
"mcpServers": {
"memorybank": {
"command": "npx",
"args": ["-y", "@maxweb4u/mcp-memorybank", "--root", "./memory_bank"]
}
}
}
Fija la versión una vez que dependas de ella — @maxweb4u/mcp-memorybank@0.2.2 — para que el servidor no
cambie de forma debajo de un proyecto que no estás mirando.
El paquete tiene ámbito porque la verificación de similitud de npm no aceptará mcp-memorybank sin ámbito:
normaliza la puntuación, lo que lo hace indistinguible del no relacionado mcp-memory-bank
ya en el registro. El comando que instala el paquete sigue siendo mcp-memorybank.
Si el proyecto aún no tiene banco, siembra uno primero. El comando escribe el esqueleto, el conjunto de gobierno y un índice registrado por sección, y un banco creado de esta manera valida limpio por construcción:
npx @maxweb4u/mcp-memorybank --root ./memory_bank --init "Project Name"
Requiere Node 22. El hook descrito en hooks/README.md también quiere jq y git.
Instalándolo una vez, para cada proyecto
Un .mcp.json por proyecto es explícito y viaja con el repositorio, por eso es la forma
anterior. La alternativa es una entrada para cada proyecto en la máquina:
claude mcp add --scope user memorybank -- npx -y @maxweb4u/mcp-memorybank --root ./memory_bank
--root ./memory_bank se resuelve contra el directorio del proyecto, así que una entrada sirve a cada proyecto
que tiene un banco.
El problema es que un servidor con ámbito de usuario no tiene interruptor de apagado por proyecto en el lado del cliente —
disabledMcpjsonServers gobierna las entradas .mcp.json y nada más. Así que el servidor decide por
sí mismo cuánta superficie obtiene un proyecto:
| El proyecto | Lo que ve | Costo por solicitud |
|---|---|---|
| tiene un banco | todo | ~6,600 tokens |
| no tiene banco | bank_init, y nada más | ~450 tokens |
está nombrado por --off, o tiene un archivo .memorybank-off junto a él | nada en absoluto | ~25 tokens |
La fila del medio es la que hace razonable una instalación global: un proyecto que nunca ha tenido un
banco paga alrededor del 7% de la superficie completa, y lo que se le ofrece — bank_init — es la única llamada
que habría tenido sentido allí de todos modos. No se necesita configurar nada para esto; es lo que el servidor
hace cuando la raíz no contiene documentos.
Hay dos formas de decir "no aquí", y difieren en quién es dueño de la decisión.
--off <path> pertenece a la máquina. Repetible, y una ruta cubre todo lo que está debajo:
claude mcp add --scope user memorybank -- npx -y @maxweb4u/mcp-memorybank \
--root ./memory_bank --off /path/to/a/repo/you/do/not/govern
Nada se escribe en el proyecto que nombra — la ruta vive en la propia configuración del cliente — así que no hay archivo que aparezca en el estado de git de ese repositorio ni nada para que un colega encuentre. Eso es lo que lo hace la forma correcta para un repositorio comercial en el que trabaja el equipo de otra persona, y para un proyecto que tiene varios bancos a diferentes profundidades: una ruta los cubre todos.
.memorybank-off pertenece al proyecto. Un archivo vacío en el directorio del proyecto, junto al
banco en lugar de dentro — mantener el servidor fuera es la decisión del proyecto, no del banco:
touch .memorybank-off
Viaja con el repositorio, que es el punto cuando el proyecto mismo quiere decir no, y la razón para no recurrir a él cuando preferirías que nadie lo supiera.
La superficie se decide una vez, cuando comienza la sesión. La excepción es bank_init: siembra un banco en un
proyecto que no tenía ninguno y el resto de las herramientas aparecen de inmediato, sin reiniciar la sesión.
Instalando el comando, conectando por proyecto
La tercera forma, y la preferible cuando varias personas o máquinas comparten los repositorios. Instala una vez:
npm i -g @maxweb4u/mcp-memorybank
Luego cada proyecto que quiera el servidor lleva un .mcp.json de seis líneas que nombra el comando en lugar
de una ruta:
{
"mcpServers": {
"memorybank": {
"command": "mcp-memorybank",
"args": ["--root", "./memory_bank"]
}
}
}
Este es el mismo archivo que al principio de esta sección con npx -y @maxweb4u/mcp-memorybank reemplazado
por el comando instalado. Lo que eso compra:
por proyecto, vía npx | con ámbito de usuario, en todas partes | comando instalado, por proyecto | |
|---|---|---|---|
| un nuevo proyecto con un banco | un archivo | funciona solo | un archivo |
| un proyecto sin banco | ~450 tokens | ~450 tokens | nada en absoluto |
| un repositorio del que mantenerse fuera | .memorybank-off | --off <path> | no agregar archivo |
| costo de inicio | ~0.6 s de npx | ~0.6 s de npx | ~0.1 s |
| una ruta absoluta en git | no | no | no |
| funciona en la máquina de un colega | sí | no | sí, después de npm i -g |
| versión | fijada en el archivo | fijada en el archivo | lo que esté instalado |
La tercera columna es la única donde un proyecto que nunca tendrá un banco no cuesta nada, y
donde mantenerse fuera de un repositorio no necesita configuración en ningún lugar — simplemente no agregas el archivo.
Paga por eso con un paso manual por proyecto, y con la versión siendo lo que npm i -g puso
allí por última vez en lugar de un número escrito.
El hook quiere la misma instalación, por una razón diferente — ver hooks/README.md.
Desde un clon
Para trabajar en el servidor mismo, o para apuntar varios proyectos a una compilación:
npm install && npm run build
node dist/cli.js --root /path/to/project/memory_bank
Un .mcp.json escrito de esta manera necesita una ruta absoluta a dist/cli.js, lo que ata el proyecto a
un checkout en una máquina. Bien mientras se desarrolla, incorrecto para cualquier cosa que planees conservar.
npx no funciona dentro de este repositorio. Desde el propio checkout del servidor,
npx @maxweb4u/mcp-memorybank falla con sh: mcp-memorybank: command not found: npx ve que el
nombre solicitado coincide con el package.json local, decide que el paquete ya está presente, y busca
el binario en el node_modules/.bin local, donde nada lo enlaza. No hay nada malo con el
paquete publicado — ejecuta el mismo comando desde cualquier otro directorio y funciona. Así que cuando quieras
verificar lo que los usuarios realmente obtienen, hazlo desde un directorio temporal, no desde aquí.
Publicar una nueva versión tiene un orden que importa — lo que se verifica en el tarball en lugar del árbol de trabajo, y qué pines se mueven antes de la publicación y cuáles solo después. Está escrito en releasing.md.
Diciéndole al agente que lo use
Conectar el servidor hace que las herramientas estén disponibles; no hace que un agente las busque. Dejado a
sí mismo, un modelo a menudo abrirá README.md y recorrerá los índices de sección, porque eso es lo que hace
en todos los demás lugares — que es exactamente el recorrido que este servidor existe para eliminar.
Un párrafo en el CLAUDE.md del proyecto lo resuelve:
# Memory bank
This project is served by the `memorybank` MCP server. **Its tools all begin with `bank_`; load them
at the start of a session and look at what is there** — the set grows, and anything not named here
still exists. Working the bank through them instead of through `sed`, `cat` and `grep` is the point:
they keep the frontmatter, the section indexes and the ownership rules intact, which a shell cannot.
- **Finding.** `bank_route` answers "which document covers this"; `bank_search` finds a literal
across the bank; `bank_read` takes a path and a section, so a long document costs one section.
Reach for those before grepping `memory_bank/`.
- **Changing.** `bank_edit` replaces one exact fragment — a backlog row, a table cell, a heading.
`bank_update_section` writes a whole named section. `bank_set_status` moves a document between
`draft`, `active` and `archived`, and releases its `canonical_for` when it retires.
- **Creating.** `bank_create` writes the frontmatter the contract asks for and registers the document
in its section index in the same operation. A document written by hand is reachable from no index.
- **Checking.** Run `bank_validate` before committing anything under `memory_bank/`, and say what it
found. Never promote a note out of `_inbox/` on your own — that is a review step for the owner.
Esta redacción no es una cuestión de gusto; fue medida. Sin tal párrafo, el mecanismo simplemente
se queda ahí — nueve horas de una sesión de trabajo con bank_route cargado y nunca llamado. Una versión
anterior del párrafo nombraba cinco herramientas explícitamente y obtuvo exactamente cinco herramientas usadas: las que
nombraba, y nada más. Nombrar el prefijo y decir que vayas a mirar, luego agrupar por trabajo en lugar de
recitar nombres, trajo todas las herramientas que la reescritura mencionó recientemente:
baseline five named rewritten
writes into the bank, shell 20 0 0
bank_edit 0 19 14
bank_route / bank_search 0/0 0/0 2/2
shell searches of the bank 18 11 8
bank_validate findings 3→5 6 0
El control hace legible la razón. Un proyecto sin CLAUDE.md en absoluto aún encontró la familia
de herramientas por sí solo — bank_edit treinta veces, bank_create dieciocho — y aún hizo siete escrituras
de shell en su propio banco, cada una con la forma exacta de las herramientas que ya estaba usando. El
servidor es lo que hace que un banco se mantenga; las instrucciones no pueden verificar nada. El párrafo es lo que hace
consistente la elección. Ninguno sustituye al otro — ver field-test.md.
Depurando desde la terminal
La CLI es un subconjunto de solo lectura para mirar un banco sin un agente en el bucle, no un espejo de
la superficie de herramientas: bank_drift, bank_edit, bank_update_section, bank_set_status y
bank_discard son solo MCP. Instalado desde npm, el comando es mcp-memorybank; desde un clon es
node dist/cli.js.
mcp-memorybank --root <bank> --stats
mcp-memorybank --root <bank> --route "filter thresholds" --limit 5
mcp-memorybank --root <bank> --read domain/rules.md --section Thresholds
mcp-memorybank --root <bank> --validate --summary
mcp-memorybank --root <bank> --validate --rule broken-derived-from
mcp-memorybank --root <bank> --graph domain/rules.md --direction down --depth 1
mcp-memorybank --root <bank> --search "FT-042" --limit 5
mcp-memorybank --root <bank> --changed HEAD~5
mcp-memorybank --root <bank> --create adr/ADR-...-name.md --kind adr --title "..." --purpose "..." --derived ../engineering/architecture.md --dry-run
mcp-memorybank --root <new-path> --init "Project Name" --dry-run
mcp-memorybank --root <bank> --materialize-flows
mcp-memorybank --root <bank> --list-inbox
mcp-memorybank --root <bank> --promote _inbox/note.md --to engineering/thing.md --derived ../dna/principles.md --dry-run
Qué hay allí
| Herramienta | Qué hace |
|---|---|
bank_route | "qué debería leer sobre esto" — clasifica según canonical_for, purpose, title, encabezados de sección |
bank_read | un documento completo, una sección de segundo nivel, o varios documentos en una sola llamada, bajo un presupuesto de bytes |
bank_validate | verifica el banco contra las reglas que el propio banco declara en dna/ |
bank_graph | recorre derived_from: down — quién depende de esto (radio de explosión), up — sobre qué está construido |
bank_drift | compara cada documento con el código que declara en anchors: e informa dónde el código va por delante |
bank_search | búsqueda de texto completo en los cuerpos de los documentos — identificadores, nombres, literales |
bank_changed | qué cambió desde una referencia git o una fecha ISO; una referencia más antigua que el repositorio aterriza en su primer commit, y un banco sin repositorio responde con todo |
bank_init | crea un banco desde cero: esqueleto, dna/, los flujos, índices de sección |
bank_materialize_flows | copia las plantillas de documentos incluidas en el banco, que a partir de entonces son de su propiedad |
bank_create | crea un documento a partir de la plantilla propia del banco y lo registra en el índice, con compuertas y _inbox |
bank_edit | reemplaza un fragmento exacto de un cuerpo — una fila de tabla, un paso, un encabezado; rechaza una coincidencia ambigua |
bank_update_section | escribe el cuerpo de una sección nombrada de un documento existente, dejando todo lo demás intacto |
bank_set_status | mueve un documento a otro estado del ciclo de vida, ejecutando las compuertas que protegen la activación |
bank_promote | saca una nota de _inbox/ hacia una capa canónica, registrándola y eliminando la fuente |
bank_discard | descarta una nota en cuarentena, de forma registrada — solo _inbox/, y se requiere una razón |
| Recurso | Contenido |
|---|---|
memorybank://index | índice anotado de cada documento (plantillas excluidas) |
memorybank://schema/frontmatter | el propio dna/frontmatter.md del banco, textualmente |
memorybank://health | una ejecución reciente de bank_validate, agrupada por regla |
memorybank://inbox | lo que está en cuarentena, de más antiguo a más reciente |
Prompts: session-start (qué es el proyecto, qué está en curso, qué está abierto — desde el índice y
el delta, no leyendo todo), route-then-read (rutear primero, leer después),
check-before-commit (ejecutar validación y explicar cada hallazgo), record-adr (ensamblar una decisión
en un ADR), review-inbox (trabajar a través de la cuarentena).
Dos de estos existen por lo que una sesión de trabajo realmente hizo, más que por lo que el diseño
esperaba. Durante una tarde, un agente llamó a bank_read cero veces y a cat en un bucle de shell 85
veces: diez documentos eran diez llamadas de herramienta en un sentido y una sola llamada en el otro, así que el alcance por sección — la
característica que mantiene los documentos grandes fuera de la ventana de contexto — perdió por aritmética antes de ser
considerado. De ahí un bank_read que acepta una lista. Y la primera pregunta de cada sesión fue
"qué es esto, dónde nos detuvimos, qué está abierto", que el ruteo no responde; sin un lugar donde
enviarla, el agente leía todo el banco. De ahí session-start.
Crear un banco
mcp-memorybank --root <new-path> --init "Project Name"
Escribe 24 archivos: dna/ (8 documentos de gobernanza, incluida la tabla de capas), los cuatro documentos de
flujo, un índice para cada una de las ocho secciones sembradas más el índice raíz, un puntero donde
estarían las plantillas, y dos borradores — product/context.md y engineering/testing-policy.md —
a los que apuntan tanto las plantillas como la compuerta de cierre de características.
Tres secciones adicionales — epics/, prd/, prompts/ — no se crean de antemano. bank_create
construye una, su índice y su entrada en el índice raíz la primera vez que un documento la necesita, y lo indica.
El conjunto inicial vive en starter/ y pertenece al banco en el momento en que se copia: reescríbelo
como quieras, el servidor no sigue imponiéndolo. Lo que no se copia es
flows/templates/ — ver Qué contiene un banco sembrado.
Un banco recién creado valida con cero hallazgos — que es el punto central de bank_init
en lugar de copiar el banco de otra persona: una copia traería los defectos de ese banco consigo.
Plantillas y la cuarentena
La mecánica de una escritura está en Cómo funciona; lo que sigue es de dónde viene el contenido y a dónde va una nota no revisada.
bank_create escribe el cuerpo que se le da y, si no, el cuerpo de la plantilla correspondiente. Pasa
body en lugar de crear el documento y escribir la prosa después — un documento que
llega solo con encabezados tiende a recibir su contenido a través de un shell, fuera de cada compuerta que el servidor tiene.
Tres herramientas acceden a un documento que ya existe, y la división entre ellas es una medida más que una ordenada.
bank_update_section escribe el cuerpo de una sección de segundo nivel nombrada, dejando el frontmatter, el
título y todas las demás secciones intactos. Existe porque bank_init siembra product/context.md y
engineering/testing-policy.md como borradores específicamente para que se llenen, mientras que bank_create
rechaza una ruta ocupada — correctamente — lo que dejó los dos documentos que el servidor pide como los dos que
no podía escribir. Una sección que no existe se rechaza con la lista de las reales en lugar de
añadirse, para que un encabezado mal escrito no pueda añadir silenciosamente una sección.
Tanto bank_update_section como bank_create tomarán su contenido de un archivo en lugar de la
llamada — contentFile y bodyFile. Eso existe por una razón medida: una sesión que construyó un banco
generó una tabla de 610 filas a partir de otros diecinueve documentos con un script y la escribió en el banco
directamente, saltándose cada compuerta. No porque la herramienta no pudiera hacer la escritura, sino porque la herramienta
quería toda la tabla como un argumento de cadena, y la tabla nunca había estado en el contexto del agente en
absoluto. El contenido que se calculó en lugar de componerse no debería tener que recitarse para ser gobernado, y
un cuerpo que no viaja como una cadena JSON no puede fallar al serializarse como una.
bank_edit reemplaza un fragmento exacto. La escritura a nivel de sección resultó ser el grano equivocado para
la mayoría de las ediciones: durante una sesión de trabajo, cinco de once escrituras de shell en el banco cambiaron unas pocas líneas
dentro de una sección de decenas de líneas — una fila de una tabla, dos pasos de un plan, un párrafo de un
argumento — y una sexta renombró un encabezado. Reemplazar toda la sección significa reenviar todo
sin cambios alrededor de la edición, así que un agente recurre a un reemplazo de cadena, cada vez. El contrato
es el que ya conoce: una coincidencia exacta, rechazada a menos que ocurra exactamente una vez, con el recuento
informado en lugar de adivinar la primera ocurrencia. section reduce la búsqueda cuando las mismas palabras
aparecen dos veces. El frontmatter está fuera de alcance por construcción, ya que la búsqueda se ejecuta en el
cuerpo — una edición puede renombrar un encabezado, pero no puede reescribir silenciosamente canonical_for.
bank_set_status mueve un documento de un estado del ciclo de vida a otro, y existe porque el
frontmatter está fuera del alcance de las otras dos. Un borrador que bank_init sembró y
bank_update_section llenó tiene que volverse activo en algún momento, y sin una herramienta para ello, un agente
recurre a un editor de flujo en el frontmatter — medido, en una sesión que construyó un banco desde
nada. Esa es la peor edición para dejar a un shell: status: active es la compuerta que requiere
derived_from, así que la única transición que la gobernanza existe para proteger fue la única transición que la omitió.
Aquí la verificación se ejecuta al entrar, y una nota en _inbox/ se rechaza de plano, ya que la salida
de la cuarentena es bank_promote, que coloca la nota y establece su estado en un solo movimiento.
El archivado es la única transición que cede algo. ownerByKey no mira el estado, así que un
documento archivado mientras aún declara canonical_for sigue bloqueando al sucesor que debería
poseer la clave — bank_create rechaza al sucesor con "ya es propiedad de", nombrando un documento que nadie
lee ya. Archivar a un propietario se rechaza por tanto a menos que releaseCanonical lo diga, y
las claves se eliminan como parte de la misma escritura. Medido, de nuevo: una sesión archivó un documento y
eliminó el bloque con una regex de python, porque esa era la única forma de hacerlo.
bank_create toma la plantilla del propio flows/templates/ del banco cuando la tiene, y del
conjunto que el servidor incluye cuando no. Las plantillas son
envoltorios: el documento a instanciar está dentro de ellas como dos bloques bajo
## Instantiated Frontmatter y ## Instantiated Body. El servidor desenvuelve exactamente esos,
sustituye el título, llena el marcador de fecha y transfiere must_not_define, que la
plantilla incluye como gobernanza. Un doc_kind desconocido es una advertencia, no un rechazo, y dryRun muestra
todo el resultado — frontmatter, plantilla elegida, línea de índice — sin escribir nada.
inbox: true coloca el documento en _inbox/ con status: draft, sin plantilla y sin
registro. Los documentos en cuarentena están exentos de la regla de unregistered-doc, y bank_route
y bank_search no los devuelven en absoluto — son accesibles mediante bank_read, el
recurso memorybank://inbox y bank_promote, y en ningún otro lugar. Atenuarlos por peso de capa se
probó primero y no es lo mismo: en un banco joven, una nota llegó a los tres primeros para la pregunta
que respondía, que es exactamente lo que una cuarentena debe prevenir.
bank_promote vacía la cuarentena: el cuerpo de la nota se conserva tal como está escrito, el frontmatter se reconstruye
contra el contrato, la plantilla de destino contribuye solo con sus campos de gobernanza, el documento
se registra en el índice y la fuente se elimina. Mismas compuertas que bank_create, más dos propias:
solo se puede promover desde _inbox/, y solo hacia afuera. El estado después de la promoción es active, así que
el requisito de gobernanza sobre derived_from ya no se relaja aquí.
bank_discard es el tercer resultado, y existe porque la segunda revisión de una cuarentena real
salió del servidor para alcanzarlo. Una nota puede promoverse, fusionarse en el documento que ya
posee el hecho con bank_update_section, o descartarse — y con solo los dos primeros implementados, el
agente fusionó una nota y luego recurrió a rm en un shell para limpiar lo que acababa de consumir. Una
herramienta que nombra tres resultados y soporta dos envía el tercero más allá de cada compuerta que tiene. _inbox/
solo, y se requiere una razón: una nota queda registrada o no queda.
La cuarentena se llena mediante un hook de Stop — ver hooks/README.md.
Consultas en dos idiomas
El banco está escrito en inglés — esa es una regla de gobernanza y nada aquí la relaja. La
pregunta es un asunto diferente: escrita a mano, a menudo no está en inglés, y la clasificación léxica sobre
campos en inglés no puede responderla en absoluto. "где пороги фильтрации" se tokeniza en tres palabras que
no aparecen en ningún purpose o canonical_for, así que el resultado está vacío en lugar de simplemente incorrecto.
bank_route y bank_search tratan por tanto una palabra de consulta como un concepto en lugar de una cadena. Cada
palabra lleva sus equivalentes en el otro idioma, extraídos de un diccionario de dominio construido a partir de
los términos más frecuentes en title / purpose / canonical_for en un cuerpo de bancos reales. Tres
cosas se siguen de esto:
- Un concepto se puntúa una sola vez. Tres equivalentes en inglés que coinciden con un
purposees un acierto, no tres, por lo que una consulta ampliada no puede superar a una literal solo por amplitud. - La palabra realmente escrita gana en caso de empate. Una traducción se descuenta, lo que solo importa en un banco que mezcla idiomas: allí la coincidencia literal ocupa el primer lugar.
- El ruso se flexiona, así que una entrada de diccionario es un prefijo de raíz, no una palabra.
порогcubre пороги / порогов / порогам; un prefijo verbal se elimina solo cuando lo que queda aterriza en una raíz conocida, lo cual es lo que permite queзадеплоитьllegue adeployment.
Medido en un banco real, una pregunta de control en ruso devuelve el mismo primer documento que su contraparte en inglés. La búsqueda es simétrica: el cirílico es un carácter de palabra en el índice, por lo que un documento que cita una fuente rusa es buscable, y una consulta en ruso llega a prosa en inglés a través del mismo diccionario.
Una palabra desconocida se deja exactamente como se escribió: identificadores como FT-042 y filter_thresholds
nunca se acercan al diccionario.
El banco puede agregar sus propias palabras. El diccionario incorporado lleva el tema del que se extrajo:
agentes, despliegue, frontend, backend. Apunta el servidor a un proyecto sobre análisis de libros y
el lado ruso se queda en silencio en корпус, книга, прогон: no una respuesta pobre, sino una vacía, mientras
la misma pregunta en inglés aterriza tres de tres. El lado inglés nunca tiene este problema
porque se lee del banco; el lado ruso no puede, ya que el banco es inglés por regla
de gobierno. Así que anótalo, en un dna/vocabulary.md opcional:
| Russian stem | English |
|---|---|
| корпус | corpus, collection |
| книг | book, books, ebook |
| сегмент | segmentation, segmenter, sentence |
La columna izquierda es una raíz: el prefijo más corto compartido por cada forma flexionada—y las filas que
no son dos columnas con un lado izquierdo cirílico se ignoran, por lo que el archivo puede llevar prosa y encabezados
alrededor de la tabla. Se vuelve a leer en cada actualización, por lo que editarlo surte efecto sin reiniciar. El
archivo no se siembra con bank_init: un banco que no necesita uno no debería llevar uno vacío.
Reglas de validación
| Regla | Severidad | Detecta |
|---|---|---|
broken-derived-from | error | un borde cuyo destino no se resuelve desde el documento que lo declara |
invalid-frontmatter | error | YAML que el analizador rechaza—generalmente un colon sin comillas en purpose |
cycle-in-derived-from | error | A deriva de B deriva de A |
ssot-conflict | error | dos documentos que reclaman la misma clave canonical_for |
missing-derived-from | error | un documento active no raíz sin upstream |
must-not-define-violated | error | un documento que define una clave que declaró que no definiría |
dangling-index-entry | error | un índice que enlaza a un archivo que no está allí |
unknown-enum-value | advertencia | un doc_kind / doc_function / status fuera de lo que dna/ declara |
unregistered-doc | advertencia | un documento al que ningún índice enlaza—inalcanzable por navegación |
unresolved-rule-reference | advertencia | prosa que cita una regla por una ruta que no se resuelve |
unknown-layer-value | advertencia | la tabla de capas en dna/ nombra una capa que no existe; la fila se ignora |
no-contract | advertencia | el banco no tiene dna/, por lo que las reglas de contrato no pueden ejecutarse |
El orden no es alfabético: es descendente según la frecuencia con la que cada regla se activó mientras se construía el validador, para que las reglas productivas se lean primero.
Las reglas estructurales funcionan en cualquier banco. Las reglas de contrato (missing-derived-from, ciclos,
unknown-enum-value) se aplican solo donde el banco las declaró él mismo en dna/governance.md—el
servidor aplica las reglas del banco, no las suyas.
Decisiones donde la implementación se aparta de la especificación
- El esquema se lee del banco.
doc_kind/doc_function/statusson conjuntos abiertos, leídos dedna/frontmatter.mdydna/governance.md. Una enumeración rígida rechaza una minoría considerable de los documentos en un banco real. - Un documento tiene una capa (
dna/knowledge/decision/delivery/flow), derivada de su ruta y ponderada en la clasificación. Sin ella, el enrutamiento falla en un banco donde el 80% de los documentos son un diario de entrega. derived_fromse resuelve relativo al documento y admite ambas formas: una cadena y{ path, fit }.- Una referencia que sale de la raíz se marca como
externalen lugar de contarse como rota: en un monorepo, los bancos se anidan. - Un documento con YAML inválido no cae fuera del índice. 22 documentos en los bancos
reales tienen un colon sin comillas en
purpose; sus metadatos se recuperan línea por línea y el error se conserva para quebank_validatelo informe. - Las plantillas se excluyen de los resultados de
bank_routey dememorybank://index. bank_routeybank_searchresponden preguntas diferentes. El primero clasifica según el encabezado escrito a mano ("de qué trata este documento"), el segundo busca en la prosa ("dónde aparece esta cadena"). En la búsqueda, un token completo pesa diez veces más que sus propios fragmentos: de lo contrario, la consultaFT-042elevaría el registro defeatures/README.md, con sus setenta líneas deFT-*, por encima de la propia característica.- Una consulta es bilingüe, el banco no. La clasificación expande una palabra de consulta a sus equivalentes en el otro idioma y puntúa el concepto una sola vez. Los documentos permanecen en inglés; solo la pregunta puede no serlo.
- El frontmatter se analiza en exactamente un lugar.
gray-mattersin opciones memoriza por texto de entrada, y después de lanzar devuelve un objeto en caché cuyo frontmatter completo reside dentro decontent—en silencio. Las opciones siempre se pasan, y el análisis pasa por un únicosplitFrontmatter. - El grafo separa tres resultados de borde: interno (un nodo), externo (
external—un banco anidado), roto (broken). Amplitud primero con un límite de nodos, para que un documento central no arrastre todo el banco y un ciclo no se repita.
Pruebas
npm test
195 pruebas en tres niveles, que existen por diferentes razones.
Generadas — test/acceptance-generated.test.ts siembra un banco con el bank_init actual, lo llena
a través de bank_create únicamente, y luego ejecuta los criterios de preparación del plan contra eso: enrutamiento en
ambos idiomas, el grafo sobre bordes que el propio servidor escribió, búsqueda, el delta de git, y cada
puerta de rechazo. No necesita nada más que un directorio temporal y git, por lo que este es el nivel que debe permanecer
verde. También lleva la afirmación más fuerte del diseño: un banco construido enteramente a través del
servidor valida con cero hallazgos, y sigue así después de tres escrituras rechazadas.
Fixture — test/fixture-bank/ es un banco con violaciones deliberadas: un borde roto, un segundo
propietario de un hecho, un huérfano, YAML roto, una referencia fuera de la raíz. Se mantiene escrito a mano a
propósito, porque bank_create no puede producir ninguno de ellos.
Corpus — test/acceptance.test.ts ejecuta los mismos criterios contra los bancos que le indiques,
que es como se midieron las decisiones de diseño en primer lugar. Esos bancos son tuyos y
viven fuera del repositorio, por lo que la suite es opcional:
MEMORYBANK_TEST_ROOTS=/path/to/projects npm run test:corpus
Con la variable sin configurar, se omite. Con ella configurada pero los bancos ausentes, falla en lugar de omitirse: un error tipográfico en una ruta no debería leerse como un pase.