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:

CampoPesoPuntuado como
canonical_for5cuánto de una clave de hecho cubre la pregunta, no si una palabra de ella coincidió
purpose3solapamiento de palabras
title2solapamiento de palabras
encabezados de sección1el 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: done o cancelled reduce 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 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 servidor34398
15 definiciones de herramientas20,4735,849
5 indicaciones1,357388
4 recursos913261
total, por solicitud23,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 banco91,31926,091
una respuesta bank_route, 3 resultados839240
los tres documentos que nombró8,3862,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 proyectoLo que veCosto por solicitud
tiene un bancotodo~6,600 tokens
no tiene bancobank_init, y nada más~450 tokens
está nombrado por --off, o tiene un archivo .memorybank-off junto a élnada 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 npxcon ámbito de usuario, en todas partescomando instalado, por proyecto
un nuevo proyecto con un bancoun archivofunciona soloun archivo
un proyecto sin banco~450 tokens~450 tokensnada 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 gitnonono
funciona en la máquina de un coleganosí, después de npm i -g
versiónfijada en el archivofijada en el archivolo 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í

HerramientaQué hace
bank_route"qué debería leer sobre esto" — clasifica según canonical_for, purpose, title, encabezados de sección
bank_readun documento completo, una sección de segundo nivel, o varios documentos en una sola llamada, bajo un presupuesto de bytes
bank_validateverifica el banco contra las reglas que el propio banco declara en dna/
bank_graphrecorre derived_from: down — quién depende de esto (radio de explosión), up — sobre qué está construido
bank_driftcompara cada documento con el código que declara en anchors: e informa dónde el código va por delante
bank_searchbúsqueda de texto completo en los cuerpos de los documentos — identificadores, nombres, literales
bank_changedqué 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_initcrea un banco desde cero: esqueleto, dna/, los flujos, índices de sección
bank_materialize_flowscopia las plantillas de documentos incluidas en el banco, que a partir de entonces son de su propiedad
bank_createcrea un documento a partir de la plantilla propia del banco y lo registra en el índice, con compuertas y _inbox
bank_editreemplaza un fragmento exacto de un cuerpo — una fila de tabla, un paso, un encabezado; rechaza una coincidencia ambigua
bank_update_sectionescribe el cuerpo de una sección nombrada de un documento existente, dejando todo lo demás intacto
bank_set_statusmueve un documento a otro estado del ciclo de vida, ejecutando las compuertas que protegen la activación
bank_promotesaca una nota de _inbox/ hacia una capa canónica, registrándola y eliminando la fuente
bank_discarddescarta una nota en cuarentena, de forma registrada — solo _inbox/, y se requiere una razón
RecursoContenido
memorybank://indexíndice anotado de cada documento (plantillas excluidas)
memorybank://schema/frontmatterel propio dna/frontmatter.md del banco, textualmente
memorybank://healthuna ejecución reciente de bank_validate, agrupada por regla
memorybank://inboxlo 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 purpose es 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 a deployment.

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

ReglaSeveridadDetecta
broken-derived-fromerrorun borde cuyo destino no se resuelve desde el documento que lo declara
invalid-frontmattererrorYAML que el analizador rechaza—generalmente un colon sin comillas en purpose
cycle-in-derived-fromerrorA deriva de B deriva de A
ssot-conflicterrordos documentos que reclaman la misma clave canonical_for
missing-derived-fromerrorun documento active no raíz sin upstream
must-not-define-violatederrorun documento que define una clave que declaró que no definiría
dangling-index-entryerrorun índice que enlaza a un archivo que no está allí
unknown-enum-valueadvertenciaun doc_kind / doc_function / status fuera de lo que dna/ declara
unregistered-docadvertenciaun documento al que ningún índice enlaza—inalcanzable por navegación
unresolved-rule-referenceadvertenciaprosa que cita una regla por una ruta que no se resuelve
unknown-layer-valueadvertenciala tabla de capas en dna/ nombra una capa que no existe; la fila se ignora
no-contractadvertenciael 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 / status son conjuntos abiertos, leídos de dna/frontmatter.md y dna/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_from se resuelve relativo al documento y admite ambas formas: una cadena y { path, fit }.
  • Una referencia que sale de la raíz se marca como external en 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 que bank_validate lo informe.
  • Las plantillas se excluyen de los resultados de bank_route y de memorybank://index.
  • bank_route y bank_search responden 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 consulta FT-042 elevaría el registro de features/README.md, con sus setenta líneas de FT-*, 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-matter sin opciones memoriza por texto de entrada, y después de lanzar devuelve un objeto en caché cuyo frontmatter completo reside dentro de content—en silencio. Las opciones siempre se pasan, y el análisis pasa por un único splitFrontmatter.
  • 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.

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

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

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