mcp-memorybank

Gerencia e navega uma base de conhecimento memory_bank: gravações controladas, roteamento sem leitura e desvio em relação ao código.

Documentação

mcp-memorybank

Um servidor MCP para uma base de conhecimento memory_bank. Ele a cria do zero, controla cada escrita de acordo com as regras que a própria base declara, roteia para o documento certo sem ler a base, e reporta onde o código avançou sem os documentos.

Ele não escreve nada por iniciativa própria. Cada documento chega por uma chamada deliberada que carrega um propósito, uma dependência upstream e uma entrada de índice; o que o hook de fim de sessão coleta cai em uma quarentena que apenas um humano esvazia.

Especificação — specification.md, plano — implementation-plan.md, do que uma base semeada é feita e onde ela se estica — architecture.md.

Tudo no plano está feito: E0 índice, E1 leituras, E2 validação, E3 grafo, E4 busca e delta, E5 escritas.

No que ele é comprovadamente bom, e no que não é

Contado em seis bases em uso real, analisando as sessões em vez de por impressão:

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

A metade de escrita funciona. bank_create, bank_edit e bank_update_section são usados em toda base, com ou sem instrução para isso, porque escrever um documento governado à mão é mais difícil do que chamar a ferramenta: seria preciso reproduzir o frontmatter, resolver o derived_from e registrar-se em um índice de seção, e falharia na validação se errasse qualquer parte disso. Noventa por cento de todas as chamadas ao servidor são escritas.

A metade de leitura é em grande parte ignorada. Nove em cada dez vezes um agente abre a base com cat e grep em vez de perguntar ao bank_route quais documentos importam. Isso não é uma afirmação de que o roteamento responde mal — ninguém mediu isso. É uma afirmação de que ele não é consultado, porque alcançar o shell é o que um modelo faz em todo repositório e nada aqui supera o hábito.

Duas coisas se seguem, e nenhuma é confortável. O roteamento é o recurso que este README lidera e o que a aritmética de tokens abaixo sustenta, e é o menos usado. Editar documentos existentes foi escrito na especificação como algo que o servidor não faria — veja §8 — e acabou sendo a metade que carrega o projeto.

A única alavanca com evidência por trás é o parágrafo sob Dizendo ao agente para usá-lo: as quatro bases que o carregam roteiam oito vezes mais do que as duas que não o fazem, 32% das leituras contra 4%. Isso é uma correlação em seis projetos, e está sendo testada adequadamente — veja A-06 em backlog.md.

Como funciona

Não há banco de dados, nem daemon, nem configuração. Um diretório de arquivos markdown é todo o estado, e o servidor é um leitor dele que por acaso fala MCP. Tudo o que ele sabe ele re-deriva dos arquivos; delete seu processo e nada se perde, porque nada foi mantido em outro lugar.

O índice, e por que não há watcher

Na inicialização, o servidor percorre a base uma vez, e em cada chamada ele a percorre novamente: readdir, stat, e um parse apenas para os arquivos cujo mtime mudou. Uma construção a frio de uma base de 368 documentos leva cerca de 85 ms; a varredura antes de uma chamada inalterada custa 11–13 ms.

É por isso que não há watcher de arquivos. Um watcher economizaria esses milissegundos e compraria uma classe de erro em troca — um índice que divergiu silenciosamente do disco, em uma ferramenta cujo trabalho inteiro é ser confiável sobre o que o disco diz. A verificação barata vence em ambos os aspectos.

O que um documento se torna

Cada arquivo é analisado uma vez em um registro: os campos do frontmatter, os cabeçalhos de seção de segundo nível com seus intervalos de linha, tamanho em bytes, mtime — e uma coisa que o arquivo não contém, uma camada calculada a partir do caminho (dna, knowledge, decision, delivery, flow, inbox, other).

O frontmatter passa por gray-matter em exatamente um lugar. Quando o YAML é inválido — um dois-pontos sem aspas em purpose é o caso comum — o documento não cai fora do índice: seus metadados são recuperados linha por linha e o erro de parse é mantido para bank_validate reportar. Um documento que desaparece do roteamento por causa de um erro de digitação é pior do que um que classifica mal.

O contrato vem da base

doc_kind, doc_function, status, se derived_from é obrigatório, qual documento é a raiz declarada — tudo isso é lido do próprio dna/frontmatter.md e dna/governance.md da base no momento da atualização. Nada é codificado, e os conjuntos são abertos.

Isso não é polidez: as bases discordam sobre seu próprio vocabulário, e uma enumeração fixa rejeita uma minoria considerável de documentos reais apenas pelo doc_kind. Uma base sem dna/ ainda funciona: o servidor roda em modo degradado — leitura, roteamento, busca e as regras estruturais — e diz isso em vez de impor um contrato que ninguém declarou.

Roteamento: classificando o cabeçalho, nunca a prosa

bank_route responde "o que devo ler sobre isso", e lê apenas o que um humano escreveu à mão sobre cada documento — nunca o corpo. Quatro campos, com pesos fixos:

CampoPesoPontuado como
canonical_for5quanto de uma chave de fato a pergunta cobre, não se uma palavra dela correspondeu
purpose3sobreposição de palavras
title2sobreposição de palavras
cabeçalhos de seção1o cabeçalho de melhor correspondência, que também é retornado para que a resposta possa ser lida com escopo de seção

A pontuação bruta é então multiplicada, e os multiplicadores são onde a classificação realmente obtém seu julgamento:

  • Camada. knowledge ×1.5, decision ×1.2, dna / flow / other ×1.0, delivery ×0.6, inbox ×0.2. A entrega é amortecida porque em uma base que está em uso há um tempo, ela é a maioria dos documentos; sem isso, uma pergunta sobre uma regra retorna os recursos fechados que a mencionam.
  • Status. active ×1, draft ×0.7, archived ×0.2.
  • Trabalho fechado. delivery_status: done ou cancelled reduz a pontuação pela metade novamente. Um recurso concluído é história, não uma resposta.
  • Intenção. Uma pergunta contendo por que, justificativa, em vez de, почему, вместо re-pondera toda a execução em direção a decisões: decision ×1.6, knowledge ×1.2. "Por que X" e "o que é X" são perguntas diferentes e não devem retornar o mesmo documento primeiro.

Modelos nunca aparecem nos resultados: eles são estruturalmente idênticos a documentos reais e inundariam toda lista.

Busca: um índice diferente para uma pergunta diferente

bank_search não é um fallback para roteamento, ele responde a outra metade. O roteamento classifica o cabeçalho escrito à mão ("qual documento é sobre isso"); a busca lê a prosa ("onde essa string realmente aparece"). Identificadores, mensagens de erro e literais vivem apenas nos corpos.

Ele constrói um índice invertido sobre os corpos dos documentos, incrementalmente na mesma verificação de mtime. Uma consulta corresponde em três níveis por confiança — a palavra como digitada pesa 10, seu equivalente no outro idioma 6, um fragmento de um token composto 1 — e documentos que correspondem a todos os conceitos são classificados antes de documentos que correspondem a alguns. O nível de fragmento é o que mantém FT-042 de elevar o registro features/README.md, com suas setenta linhas de FT-*, acima do próprio recurso.

Escrita: uma operação, ou nenhuma

bank_create escreve o documento, preenche o frontmatter que o contrato pede e o registra no índice de seção na mesma chamada — para que a etapa de registro não possa ser esquecida, que é a maneira mais comum de uma base apodrecer. O registro copia a forma da última entrada naquele índice, linha de tabela ou marcador ou item numerado, para que um arquivo escrito à mão não seja reformatado.

Antes de tudo isso, ele recusa, com o motivo nomeado: o caminho está ocupado, derived_from não resolve, canonical_for já é propriedade de outro documento, o caminho sai da raiz da base, ou não termina em .md. Uma recusa não escreve nada — sem arquivo parcial, sem linha de índice órfã.

Duas regras seguem dessas portas, e são a razão pela qual as escritas valem a pena: um conflito SSoT e uma aresta quebrada não podem entrar na base através deste servidor. Elas só podem chegar editando um arquivo pelas costas dele.

O grafo

bank_graph percorre derived_from em largura com um teto de nós, para que um documento hub não arraste toda a base e um ciclo não faça loop. Cada aresta cai em um de três resultados: interno (um nó), external (sai da raiz da base — em um monorepo, as bases se aninham), ou broken. up é o que um documento é construído; down é o raio de explosão de mudá-lo.

Uma aresta externa é seguida exatamente um salto: o frontmatter do alvo é lido e retornado sob neighbours, e nada mais sobre ele é. Não é indexado, não é validado, não é pesquisável, e suas próprias arestas não são percorridas. A fronteira permanece onde estava — mas em um monorepo essas arestas carregam decisões reais, e um raio de explosão que termina silenciosamente na parede do repositório é errado em vez de parcial. Passe neighbours: false para pular as leituras.

Deriva

bank_drift responde à pergunta que a validação não pode: não "algo está faltando", mas "o que escrevemos ficou desatualizado". Um documento que descreve código o lista em anchors:, e o servidor compara o último commit que tocou o documento contra o último commit que tocou o código. Onde o código está à frente por mais do que o limite, ele diz isso — e onde uma âncora aponta para um caminho que não existe mais, ele sempre diz isso, porque isso é o código se movendo para fora de baixo do documento.

Ele nunca adivinha qual documento é dono de qual arquivo. O pareamento é anotado à mão ou não existe, o que significa que uma base que não foi anotada recebe uma resposta vazia honesta e uma nota dizendo por quê. E ele reporta sem editar: se uma lacuna de seis meses importa não é um julgamento que um timestamp pode fazer.

O que uma base semeada contém

bank_init escreve 24 arquivos: o conjunto de governança em dna/, um índice raiz, oito registros de seção, os quatro documentos de fluxo, um ponteiro onde os modelos estariam e dois rascunhos para preencher. Mais três seções — epics, prd, prompts — são construídas na primeira vez que um documento precisa de uma, incluindo índice e entrada de índice raiz, em vez de ficarem vazias desde o início.

Os modelos de documento não são copiados. Eles acompanham o servidor e bank_create os lê de lá, para que uma base se comporte exatamente como se os tivesse; --materialize-flows os copia para um projeto que pretende mudar um, e a partir de então as cópias da própria base vencem. Os fluxos de prosa são copiados, porque as pessoas os leem e o roteamento responde com eles.

dna/ também é copiado, e esse não é negociável: é a lei pela qual a base é julgada, e o design depende dele viajar com o corpus. A camada de cada diretório de nível superior e o peso de classificação são declarados lá também — uma base que renomeia uma seção diz isso nessa tabela e mantém seu peso, e uma base que não declara nada recebe o mapa embutido.

O que ele não fará

Ele não cria um documento por edição — cada novo documento passa por bank_create, então nada entra na base sem uma verificação de caminho, um derived_from que resolve e uma entrada de índice. Ele não promove nada para fora de _inbox/ por conta própria. Ele não inventa um esquema, e não impõe uma regra que a base não declarou. Ele não exclui um documento fora de _inbox/. E ele não indexa, valida ou busca nada fora da raiz da base — o único salto que bank_graph dá para uma base vizinha lê um cabeçalho e para ali.

O que custa em tokens

Wiring a server in is not free, and the cost is paid in two different currencies. All figures below were measured on the shipped build, counting characters of the actual JSON-RPC payloads and converting at 3.5 characters per token — dense JSON runs closer to that than the 4 that suits prose.

The fixed half is paid on every request, whether a tool is called or not, because the definitions live in the context:

characters~tokens
server instructions34398
15 tool definitions20,4735,849
5 prompts1,357388
4 resources913261
total, per request23,086~6,600

bank_create é a definição mais cara, com ~715 tokens — sete parâmetros e uma descrição que precisa explicar o portão. bank_changed é a mais barata, com ~183.

Essa tabela é o que um projeto com um banco paga. Um projeto que não tem nenhum recebe apenas bank_init e paga ~450; um que está desligado — por --off ou um arquivo .memorybank-off — paga ~25 por uma superfície vazia. Veja Instalando uma vez, para cada projeto — a razão pela qual o servidor se dá ao trabalho de fazer essa distinção é que uma instalação global cobraria a taxa cheia em todos os projetos da máquina, incluindo todos os projetos que nunca terão um banco.

A metade variável é onde ele recupera a metade fixa. Em um banco com 27 documentos:

characters~tokens
lendo o banco inteiro91,31926,091
uma resposta bank_route, 3 resultados839240
os três documentos que ela nomeou8,3862,396

Roteando respostas em 240 tokens, o que ler o corpus custa 26,091 — e um décimo do que custa ler até mesmo os três documentos corretos, porque retorna caminhos, títulos, purpose e uma linha de raciocínio em vez de qualquer prosa. O que ler depois é então uma decisão tomada com base em evidências.

bank_read é limitado separadamente: 40.000 bytes por lote, aproximadamente 11.000 tokens. Peça vinte documentos e você recebe o que cabe mais uma lista do que o orçamento não alcançou, dividido por seção, para que uma única chamada não inunde o contexto.

Onde está o ponto de equilíbrio. Os ~6.600 são pagos na primeira vez que o servidor evita uma leitura desnecessária, e isso acontece imediatamente em qualquer banco grande o suficiente para importar. Medido em quatro bancos da mesma linhagem:

 27 documents    ~26k tokens to read whole
 78 documents    ~87k
 96 documents   ~158k
112 documents   ~305k

No topo dessa faixa, o corpus tem duas janelas de contexto completas e lê-lo não é uma opção a qualquer preço, enquanto uma resposta roteada permanece na casa das centenas baixas de tokens.

A conclusão honesta é que em um banco pequeno o servidor perde. Enquanto o corpus ainda cabe em uma janela de contexto, cat é mais barato do que uma cobrança fixa de 6.600 tokens, e o teste de campo mediu exatamente isso: em um banco pequeno o suficiente para ser lido inteiro, o roteamento compete com "já está no contexto" e perde. O servidor ganha seu custo em corpora que cresceram além de serem lidos.

E tokens não são a principal coisa sendo comprada. Metade das ferramentas não economiza nada — bank_create, bank_validate e bank_drift apenas gastam. Elas existem para que o banco mantenha sua forma, o que nenhuma quantidade de orçamento de contexto fará sozinho. A economia no roteamento é um efeito colateral, não o ponto.

Executando

Conectá-lo a um projeto — <project>/.mcp.json:

{
  "mcpServers": {
    "memorybank": {
      "command": "npx",
      "args": ["-y", "@maxweb4u/mcp-memorybank", "--root", "./memory_bank"]
    }
  }
}

Fixe a versão assim que depender dela — @maxweb4u/mcp-memorybank@0.2.2 — para que o servidor não mude de forma sob um projeto que você não está olhando.

O pacote tem escopo porque a verificação de similaridade do npm não aceitará mcp-memorybank sem escopo: ele normaliza a pontuação, o que o torna indistinguível do não relacionado mcp-memory-bank já no registro. O comando que o pacote instala ainda é mcp-memorybank.

Se o projeto ainda não tem banco, crie um primeiro. O comando escreve o esqueleto, o conjunto de governança e um índice registrado por seção, e um banco criado dessa forma valida limpo por construção:

npx @maxweb4u/mcp-memorybank --root ./memory_bank --init "Project Name"

Requer Node 22. O hook descrito em hooks/README.md também quer jq e git.

Instalando uma vez, para cada projeto

Um .mcp.json por projeto é explícito e viaja com o repositório, que é por isso que tem a forma acima. A alternativa é uma entrada para cada projeto na máquina:

claude mcp add --scope user memorybank -- npx -y @maxweb4u/mcp-memorybank --root ./memory_bank

--root ./memory_bank resolve em relação ao diretório do projeto, então uma entrada atende a todos os projetos que têm um banco.

O problema é que um servidor com escopo de usuário não tem um interruptor de desligamento por projeto no lado do cliente — disabledMcpjsonServers governa entradas .mcp.json e nada mais. Então o servidor decide por si mesmo quanta superfície um projeto recebe:

O projetoO que ele vêCusto por requisição
tem um bancotudo~6.600 tokens
não tem bancobank_init, e nada mais~450 tokens
é nomeado por --off, ou tem um arquivo .memorybank-off ao ladonada~25 tokens

A linha do meio é a que torna uma instalação global razoável: um projeto que nunca teve um banco paga cerca de 7% da superfície completa, e o que é oferecido — bank_init — é a única chamada que teria feito sentido ali de qualquer forma. Nada precisa ser configurado para isso; é o que o servidor faz quando a raiz não contém documentos.

Há duas maneiras de dizer "não aqui", e elas diferem em quem é dono da decisão.

--off <path> pertence à máquina. Repetível, e um caminho cobre tudo abaixo dele:

claude mcp add --scope user memorybank -- npx -y @maxweb4u/mcp-memorybank \
  --root ./memory_bank --off /path/to/a/repo/you/do/not/govern

Nada é escrito no projeto que ele nomeia — o caminho vive na própria configuração do cliente — então não há arquivo para aparecer no git status desse repositório e nada para um colega encontrar. Isso é o que o torna a forma certa para um repositório comercial em que a equipe de outra pessoa trabalha, e para um projeto com vários bancos em profundidades diferentes: um caminho cobre todos.

.memorybank-off pertence ao projeto. Um arquivo vazio no diretório do projeto, ao lado do banco em vez de dentro dele — manter o servidor fora é a decisão do projeto, não do banco:

touch .memorybank-off

Ele viaja com o repositório, que é o ponto quando o próprio projeto quer dizer não, e a razão para não recorrer a ele quando você preferiria que ninguém soubesse.

A superfície é decidida uma vez, quando a sessão começa. A exceção é bank_init: crie um banco em um projeto que não tinha nenhum e o resto das ferramentas aparece imediatamente, sem reiniciar a sessão.

Instalando o comando, conectando por projeto

A terceira forma, e a preferida quando várias pessoas ou máquinas compartilham os repositórios. Instale uma vez:

npm i -g @maxweb4u/mcp-memorybank

Então cada projeto que quer o servidor carrega um .mcp.json de seis linhas nomeando o comando em vez de um caminho:

{
  "mcpServers": {
    "memorybank": {
      "command": "mcp-memorybank",
      "args": ["--root", "./memory_bank"]
    }
  }
}

Este é o mesmo arquivo do topo desta seção com npx -y @maxweb4u/mcp-memorybank substituído pelo comando instalado. O que isso compra:

por projeto, via npxescopo de usuário, em todos os lugarescomando instalado, por projeto
um novo projeto com um bancoum arquivofunciona sozinhoum arquivo
um projeto sem banco~450 tokens~450 tokensnada
um repositório para ficar fora.memorybank-off--off <path>não adicione arquivo
custo de inicialização~0,6 s de npx~0,6 s de npx~0,1 s
um caminho absoluto no gitnãonãonão
funciona na máquina de um colegasimnãosim, após npm i -g
versãofixada no arquivofixada no arquivoo que estiver instalado

A terceira coluna é a única onde um projeto que nunca terá um banco não custa nada, e onde ficar fora de um repositório não precisa de configuração em lugar nenhum — você simplesmente não adiciona o arquivo. Isso paga com um passo manual por projeto, e com a versão sendo o que npm i -g colocou lá por último, em vez de um número escrito.

O hook quer a mesma instalação, por uma razão diferente — veja hooks/README.md.

A partir de um clone

Para trabalhar no próprio servidor, ou para apontar vários projetos para um build:

npm install && npm run build
node dist/cli.js --root /path/to/project/memory_bank

Um .mcp.json escrito dessa forma precisa de um caminho absoluto para dist/cli.js, o que amarra o projeto a um checkout em uma máquina. Tudo bem durante o desenvolvimento, errado para qualquer coisa que você pretende manter.

npx não funciona dentro deste repositório. Do próprio checkout do servidor, npx @maxweb4u/mcp-memorybank falha com sh: mcp-memorybank: command not found: o npx vê que o nome solicitado corresponde ao package.json local, decide que o pacote já está presente, e procura pelo binário no node_modules/.bin local, onde nada o vincula. Nada está errado com o pacote publicado — execute o mesmo comando de qualquer outro diretório e funciona. Então, quando você quiser verificar o que os usuários realmente recebem, faça isso de um diretório temporário, não daqui.

Publicar uma nova versão tem uma ordem que importa — o que é verificado no tarball em vez da árvore de trabalho, e quais pins se movem antes da publicação e quais apenas depois. Está escrito em releasing.md.

Dizendo ao agente para usá-lo

Conectar o servidor torna as ferramentas disponíveis; não faz um agente alcançá-las. Deixado por conta própria, um modelo frequentemente abrirá README.md e percorrerá os índices de seção, porque é o que faz em todos os outros lugares — que é exatamente a caminhada que este servidor existe para remover.

Um parágrafo no CLAUDE.md do projeto resolve isso:

# 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 redação não é uma questão de gosto; foi medida. Sem tal parágrafo, o mecanismo simplesmente fica lá — nove horas de uma sessão de trabalho com bank_route carregado e nunca chamado. Uma versão anterior do parágrafo nomeava cinco ferramentas explicitamente e obteve exatamente cinco ferramentas usadas: as que nomeava, e nada mais. Nomear o prefixo e dizer para ir olhar, depois agrupar por tarefa em vez de recitar nomes, trouxe todas as ferramentas que a reescrita mencionou recentemente:

                              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

O controle torna a razão legível. Um projeto sem nenhum CLAUDE.md ainda encontrou a família de ferramentas por conta própria — bank_edit trinta vezes, bank_create dezoito — e ainda fez sete escritas de shell em seu próprio banco, cada uma moldada exatamente como as ferramentas que já estava usando. O servidor é o que faz um banco se manter; instruções não podem verificar nada. O parágrafo é o que torna a escolha consistente. Nenhum substitui o outro — veja field-test.md.

Depurando a partir do terminal

O CLI é um subconjunto principalmente de leitura para olhar um banco sem um agente no loop, não um espelho da superfície de ferramentas: bank_drift, bank_edit, bank_update_section, bank_set_status e bank_discard são apenas MCP. Instalado do npm, o comando é mcp-memorybank; de um clone, é 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

O que existe

FerramentaO que ela faz
bank_route"o que devo ler sobre isso" — ranqueia por canonical_for, purpose, title, títulos de seção
bank_readum documento inteiro, uma seção de segundo nível, ou vários documentos em uma única chamada, sob um orçamento de bytes
bank_validateverifica o banco contra as regras que o próprio banco declara em dna/
bank_graphpercorre derived_from: down — de quem isso depende (raio de impacto), up — sobre o que isso é construído
bank_driftcompara cada documento com o código que ele declara em anchors: e relata onde o código está à frente
bank_searchbusca em texto completo nos corpos dos documentos — identificadores, nomes, literais
bank_changedo que mudou desde uma referência git ou uma data ISO; uma referência mais antiga que o repositório cai no primeiro commit, e um banco sem repositório algum responde com tudo
bank_initcria um banco do nada: esqueleto, dna/, os fluxos, índices de seção
bank_materialize_flowscopia os modelos de documento fornecidos para o banco, que então passa a ser dono deles
bank_createcria um documento a partir do modelo do próprio banco e o registra no índice, com portões e _inbox
bank_editsubstitui exatamente um fragmento de um corpo — uma linha de tabela, um passo, um título; recusa uma correspondência ambígua
bank_update_sectionescreve o corpo de uma seção nomeada de um documento existente, deixando todo o resto intacto
bank_set_statusmove um documento para outro status de ciclo de vida, executando os portões que protegem a ativação
bank_promotemove uma nota para fora de _inbox/ para uma camada canônica, registrando-a e excluindo a origem
bank_discarddescarta uma nota em quarentena, de forma registrada — apenas _inbox/, e um motivo é obrigatório
RecursoConteúdo
memorybank://indexíndice anotado de todos os documentos (modelos excluídos)
memorybank://schema/frontmattero próprio dna/frontmatter.md do banco, na íntegra
memorybank://healthuma execução nova de bank_validate, agrupada por regra
memorybank://inboxo que está em quarentena, do mais antigo ao mais recente

Prompts: session-start (o que é o projeto, o que está em andamento, o que está em aberto — a partir do índice e do delta, não lendo tudo), route-then-read (rotear primeiro, ler depois), check-before-commit (executar validação e explicar cada achado), record-adr (montar uma decisão em um ADR), review-inbox (trabalhar a quarentena).

Dois deles existem por causa do que uma sessão de trabalho realmente fez, e não do que o design esperava. Numa tarde, um agente chamou bank_read zero vezes e cat em um loop de shell 85 vezes: dez documentos eram dez chamadas de ferramenta de um jeito e uma chamada do outro, então o escopo de seção — o recurso que mantém documentos grandes fora da janela de contexto — perdeu na aritmética antes de ser considerado. Daí um bank_read que aceita uma lista. E a primeira pergunta de toda sessão era "o que é isso, onde paramos, o que está em aberto", o que o roteamento não responde; sem um lugar para enviá-la, o agente lia o banco inteiro. Daí session-start.

Criando um banco

mcp-memorybank --root <new-path> --init "Project Name"

Escreve 24 arquivos: dna/ (8 documentos de governança, incluindo a tabela de camadas), os quatro fluxos documentos, um índice para cada uma das oito seções semeadas mais o índice raiz, um ponteiro onde os modelos estariam, e dois rascunhos — product/context.md e engineering/testing-policy.md — que tanto os modelos quanto o portão de fechamento de recurso apontam.

Três seções adicionais — epics/, prd/, prompts/ — não são criadas antecipadamente. bank_create constrói uma, seu índice e sua entrada no índice raiz na primeira vez que um documento precisar dela, e avisa.

O conjunto inicial vive em starter/ e pertence ao banco no momento em que é copiado: reescreva-o como quiser, o servidor não continua impondo-o. O que não é copiado é flows/templates/ — veja O que um banco semeado contém.

Um banco recém-criado valida com zero achados — que é exatamente o ponto de bank_init em vez de copiar o banco de outra pessoa: uma cópia traria os defeitos daquele banco junto.

Modelos e a quarentena

A mecânica de uma escrita está em Como funciona; o que segue é de onde vem o conteúdo e para onde vai uma nota não revisada.

bank_create escreve o corpo que recebe e, na falta disso, o corpo do modelo correspondente. Passe body em vez de criar o documento e escrever a prosa depois — um documento que chega apenas com títulos tende a ter seu conteúdo preenchido via shell, fora de todos os portões que o servidor tem.

Três ferramentas alcançam um documento que já existe, e a divisão entre elas é medida em vez de organizada.

bank_update_section escreve o corpo de uma seção de segundo nível nomeada, deixando o frontmatter, o título e todas as outras seções intactos. Ela existe porque bank_init semeia product/context.md e engineering/testing-policy.md como rascunhos especificamente para serem preenchidos, enquanto bank_create recusa um caminho ocupado — corretamente — o que deixou os dois documentos que o servidor pede como os dois que não conseguia escrever. Uma seção que não existe é recusada com a lista das reais em vez de ser anexada, então um título digitado errado não pode adicionar uma seção silenciosamente.

Tanto bank_update_section quanto bank_create aceitam seu conteúdo de um arquivo em vez da chamada — contentFile e bodyFile. Isso existe por um motivo medido: uma sessão que construiu um banco gerou uma tabela de 610 linhas a partir de dezenove outros documentos com um script e a escreveu no banco diretamente, contornando todos os portões. Não porque a ferramenta não conseguisse fazer a escrita, mas porque a ferramenta queria a tabela inteira como um argumento de string, e a tabela nunca tinha estado no contexto do agente de forma alguma. Conteúdo que foi calculado em vez de composto não deveria precisar ser recitado para ser governado, e um corpo que não viaja como uma string JSON não pode falhar ao serializar como uma.

bank_edit substitui exatamente um fragmento. Escrita em nível de seção acabou sendo o grão errado para a maioria das edições: ao longo de uma sessão de trabalho, cinco de onze escritas via shell no banco mudaram algumas linhas dentro de uma seção de dezenas de linhas — uma linha de tabela, dois passos de um plano, um parágrafo de um argumento — e uma sexta renomeou um título. Substituir a seção inteira significa reenviar tudo inalterado ao redor da edição, então um agente recorre a uma substituição de string, toda vez. O contrato é o que ele já conhece: uma correspondência exata, recusada a menos que ocorra exatamente uma vez, com a contagem relatada em vez de adivinhar a primeira ocorrência. section estreita a busca quando as mesmas palavras aparecem duas vezes. O frontmatter está fora de alcance por construção, já que a busca roda no corpo — uma edição pode renomear um título, mas não pode reescrever silenciosamente canonical_for.

bank_set_status move um documento de um status de ciclo de vida para outro, e existe porque o frontmatter está fora do alcance das outras duas. Um rascunho que bank_init semeou e bank_update_section preencheu precisa se tornar ativo em algum momento, e sem uma ferramenta para isso, um agente recorre a um editor de fluxo no frontmatter — medido, em uma sessão que construiu um banco do nada. Essa é a pior edição para deixar para um shell: status: active é o portão que exige derived_from, então a única transição que a governança existe para proteger foi a única transição que a ignorou. Aqui a verificação roda na entrada, e uma nota em _inbox/ é recusada de imediato, já que a saída da quarentena é bank_promote, que coloca a nota e define seu status em um único movimento.

Arquivar é a única transição que abre mão de algo. ownerByKey não olha para o status, então um documento arquivado enquanto ainda declara canonical_for continua bloqueando o sucessor que deveria possuir a chave — bank_create recusa o sucessor com "já possuído por", nomeando um documento que ninguém lê mais. Arquivar um proprietário é portanto recusado a menos que releaseCanonical diga isso, e as chaves são removidas como parte da mesma escrita. Medido, novamente: uma sessão arquivou um documento e removeu o bloqueio com um regex em python, porque essa era a única maneira de fazer isso.

bank_create pega o modelo do próprio flows/templates/ do banco quando ele tem um, e do conjunto que o servidor fornece quando não tem. Modelos são invólucros: o documento a instanciar fica dentro deles como dois blocos sob ## Instantiated Frontmatter e ## Instantiated Body. O servidor desembrulha exatamente esses, substitui o título, preenche o espaço reservado de data e carrega must_not_define, que o modelo fornece como governança. Um doc_kind desconhecido é um aviso, não uma recusa, e dryRun mostra o resultado inteiro — frontmatter, modelo escolhido, linha do índice — sem escrever nada.

inbox: true coloca o documento em _inbox/ com status: draft, sem modelo e sem registro. Documentos em quarentena estão isentos da regra de unregistered-doc, e bank_route e bank_search não os retornam de forma alguma — eles são alcançáveis por bank_read, o recurso memorybank://inbox e bank_promote, e em nenhum outro lugar. Amortecê-los pelo peso da camada foi tentado primeiro e não é a mesma coisa: em um banco jovem, uma nota chegou ao top três para a pergunta que respondia, que é exatamente o que uma quarentena deveria prevenir.

bank_promote esvazia a quarentena: o corpo da nota é mantido como escrito, o frontmatter é reconstruído contra o contrato, o modelo de destino contribui apenas com seus campos de governança, o documento é registrado no índice e a origem é excluída. Mesmos portões que bank_create, mais dois próprios: você só pode promover para fora de _inbox/, e apenas para fora. Status após a promoção é active, então o requisito de governança sobre derived_from não está mais relaxado aqui.

bank_discard é o terceiro resultado, e existe porque a segunda revisão de uma quarentena real foi para fora do servidor para alcançá-lo. Uma nota pode ser promovida, dobrada no documento que já possui o fato com bank_update_section, ou descartada — e com apenas os dois primeiros implementados, o agente dobrou uma nota e depois recorreu a rm em um shell para limpar o que acabara de consumir. Uma ferramenta que nomeia três resultados e suporta dois envia o terceiro para além de todos os portões que tem. _inbox/ apenas, e um motivo é obrigatório: uma nota vai para o registro ou não vai.

A quarentena é preenchida por um hook Stop — veja hooks/README.md.

Consultas em duas línguas

O banco é escrito em inglês — isso é uma regra de governança e nada aqui a relaxa. A pergunta é outra questão: digitada à mão, muitas vezes não é inglês, e o ranqueamento lexical sobre campos em inglês não consegue respondê-la de forma alguma. "где пороги фильтрации" tokeniza em três palavras que não aparecem em nenhum purpose ou canonical_for, então o resultado é vazio em vez de meramente errado.

bank_route e bank_search portanto tratam uma palavra de consulta como um conceito em vez de uma string. Cada palavra carrega seus equivalentes na outra língua, extraídos de um dicionário de domínio construído a partir dos termos mais frequentes em title / purpose / canonical_for em um corpo de bancos reais. Três coisas se seguem:

  • Um conceito pontua uma vez. Três equivalentes em inglês correspondendo a um único purpose é uma ocorrência, não três, então uma consulta expandida não pode superar uma literal apenas por amplitude.
  • A palavra realmente digitada vence em empate. Uma tradução é descontada, o que só importa em um banco que mistura idiomas — lá a correspondência literal fica em primeiro lugar.
  • O russo flexiona, então uma entrada de dicionário é um prefixo de radical, não uma palavra. порог cobre пороги / порогов / порогам; um prefixo verbal é removido apenas quando o que resta cai em um radical conhecido, o que é o que permite que задеплоить alcance deployment.

Medido em um banco real, uma pergunta de controle em russo retorna o mesmo primeiro documento que sua contraparte em inglês. A busca é simétrica: cirílico é um caractere de palavra no índice, então um documento citando uma fonte russa é pesquisável, e uma consulta em russo alcança prosa em inglês através do mesmo dicionário.

Uma palavra desconhecida é deixada exatamente como digitada — identificadores como FT-042 e filter_thresholds nunca chegam perto do dicionário.

O banco pode adicionar suas próprias palavras. O dicionário embutido carrega o assunto do qual foi extraído — agentes, implantação, frontend, backend. Aponte o servidor para um projeto sobre parsing de livros e o lado russo fica silencioso em корпус, книга, прогон: não uma resposta ruim, mas uma vazia, enquanto a mesma pergunta em inglês acerta três de três. O lado inglês nunca tem esse problema porque é lido do banco; o lado russo não pode ser, já que o banco é em inglês por regra de governança. Então anote isso, em um dna/vocabulary.md opcional:

| Russian stem | English |
|---|---|
| корпус | corpus, collection |
| книг | book, books, ebook |
| сегмент | segmentation, segmenter, sentence |

A coluna esquerda é um radical — o prefixo mais curto compartilhado por toda forma flexionada — e linhas que não são duas colunas com um lado esquerdo cirílico são ignoradas, então o arquivo pode carregar prosa e cabeçalhos ao redor da tabela. Ele é relido a cada atualização, então editá-lo tem efeito sem reiniciar. O arquivo não é semeado por bank_init: um banco que não precisa de um não deve carregar um vazio.

Regras de validação

RegraSeveridadeCaptura
broken-derived-fromerrouma aresta cujo alvo não resolve a partir do documento que a declara
invalid-frontmattererroYAML que o parser rejeita — geralmente dois pontos sem aspas em purpose
cycle-in-derived-fromerroA deriva de B deriva de A
ssot-conflicterrodois documentos reivindicando a mesma chave canonical_for
missing-derived-fromerroum documento active não raiz sem upstream
must-not-define-violatederroum documento definindo uma chave que declarou que não definiria
dangling-index-entryerroum índice linkando para um arquivo que não existe
unknown-enum-valueavisoum doc_kind / doc_function / status fora do que dna/ declara
unregistered-docavisoum documento ao qual nenhum índice linka — inalcançável por navegação
unresolved-rule-referenceavisoprosa citando uma regra por um caminho que não resolve
unknown-layer-valueavisoa tabela de camadas em dna/ nomeia uma camada que não existe; a linha é ignorada
no-contractavisoo banco não tem dna/, então regras de contrato não podem rodar

A ordem não é alfabética: é decrescente por quantas vezes cada regra disparou enquanto o validador estava sendo construído, então as regras produtivas são lidas primeiro.

Regras estruturais funcionam em qualquer banco. Regras de contrato (missing-derived-from, ciclos, unknown-enum-value) se aplicam apenas onde o banco as declarou em dna/governance.md — o servidor aplica as regras do banco, não as suas próprias.

Decisões onde a implementação diverge da especificação

  • O esquema é lido do banco. doc_kind / doc_function / status são conjuntos abertos, lidos de dna/frontmatter.md e dna/governance.md. Uma enumeração rígida rejeita uma minoria considerável de documentos em um banco real.
  • Um documento tem uma camada (dna / knowledge / decision / delivery / flow), derivada de seu caminho e ponderada na classificação. Sem ela, o roteamento falha em um banco onde 80% dos documentos são um diário de entrega.
  • derived_from resolve relativo ao documento e suporta ambas as formas — uma string e { path, fit }.
  • Uma referência que sai da raiz é marcada como external em vez de contada como quebrada: em um monorepo, bancos se aninham.
  • Um documento com YAML inválido não sai do índice. 22 documentos nos bancos reais têm dois pontos sem aspas em purpose; seus metadados são recuperados linha por linha e o erro é mantido para bank_validate relatar.
  • Modelos são excluídos dos resultados de bank_route e de memorybank://index.
  • bank_route e bank_search respondem perguntas diferentes. O primeiro classifica pelo cabeçalho escrito à mão ("sobre o que é este documento"), o segundo busca na prosa ("onde esta string aparece"). Na busca, um token inteiro pesa dez vezes seus próprios fragmentos: caso contrário, a consulta FT-042 elevaria o registro features/README.md, com suas setenta linhas FT-*, acima da própria funcionalidade.
  • Uma consulta é bilíngue, o banco não é. A classificação expande uma palavra da consulta em seus equivalentes no outro idioma e pontua o conceito uma vez. Documentos permanecem em inglês; apenas a pergunta pode não ser.
  • Frontmatter é analisado em exatamente um lugar. gray-matter sem opções memoiza por texto de entrada, e após lançar retorna um objeto em cache cujo frontmatter inteiro está dentro de content — silenciosamente. Opções são sempre passadas, e a análise passa por um único splitFrontmatter.
  • O grafo separa três resultados de aresta: interno (um nó), externo (external — um banco aninhado), quebrado (broken). Busca em largura com um teto de nós, então um documento hub não arrasta o banco inteiro e um ciclo não faz loop.

Testes

npm test

195 testes em três camadas, que existem por razões diferentes.

Geradostest/acceptance-generated.test.ts semeia um banco com o bank_init atual, preenche-o através de bank_create apenas, e então roda os critérios de prontidão do plano contra isso: roteamento em ambos os idiomas, o grafo sobre arestas que o próprio servidor escreveu, busca, o delta do git, e cada portão de recusa. Não precisa de nada além de um diretório temporário e git, então esta é a camada que tem que permanecer verde. Ela também carrega a afirmação mais forte do design — um banco construído inteiramente através do servidor valida com zero achados, e ainda valida após três escritas recusadas.

Fixturetest/fixture-bank/ é um banco com violações deliberadas: uma aresta quebrada, um segundo dono de um fato, um órfão, YAML quebrado, uma referência fora da raiz. Ele permanece escrito à mão de propósito, porque bank_create não pode produzir nenhum deles.

Corpustest/acceptance.test.ts roda os mesmos critérios contra quaisquer bancos que você apontar para ele, que é como as decisões de design foram medidas em primeiro lugar. Esses bancos são seus e vivem fora do repositório, então a suíte é opt-in:

MEMORYBANK_TEST_ROOTS=/path/to/projects npm run test:corpus

Com a variável não definida, ela pula. Com ela definida mas os bancos ausentes, ela falha em vez de pular — um erro de digitação em um caminho não deve ser lido como um passe.