Um agente de IA não tem véspera. Cada sessão nova abre com a mesma ignorância da primeira: não sabe onde mora a autenticação, não sabe que aquele módulo com nome de utilitário é o coração do domínio, não sabe que o arquivo de 6 mil linhas só delega. Para trocar três linhas ele precisa antes reconstruir o mapa — e reconstruir o mapa é grep, ler arquivo inteiro, seguir import, ler outro arquivo inteiro. É o que faz um desenvolvedor no primeiro dia, com a diferença de que o desenvolvedor faz isso uma vez.
O Codebase Memory MCP indexa o repositório num grafo de conhecimento em SQLite e o expõe por MCP, para que a sessão de amanhã pergunte ao grafo o que a de hoje descobriu lendo arquivo. Em vez de descrever a promessa, medi.1
Quatro perguntas, duas rotas
Peguei um repositório público que qualquer leitor pode baixar — o
fastapi/fastapi no commit 50113da,
1.138 arquivos .py e 112.998 linhas — e fiz quatro
perguntas que aparecem de verdade quando se chega num código
desconhecido. Cada uma respondida duas vezes: pelo grafo, e pela rota
que um agente sem grafo é obrigado a seguir.
| Pergunta | Pelo grafo | Lendo arquivo | Razão |
|---|---|---|---|
| Onde eu começo? | 3.930 Bget_architecture |
80.459 B README + árvore de .py |
20× |
| Quem chama isto, e o que ela chama? | 3.528 Bsearch_graph + trace_path |
303.462 B grep + 3 arquivos |
86× |
| O que meu diff afeta? | 544 Bdetect_changes |
295.629 Bgit diff + 2 arquivos |
543× |
| Que função não tem chamador? | 43 Bquery_graph |
não responde | — |
- pelo grafo
- lendo arquivo
Somando as quatro, a rota do grafo devolveu 8.045 bytes e a rota manual 679.550. Traduzido para a moeda que importa numa sessão de agente — contexto — são cerca de 2 mil tokens contra 170 mil.2
E a diferença não é só de volume. O grep devolve posições de texto; o grafo devolve relações. São perguntas de tipo diferente, e a quarta linha da tabela é a prova: "que função não tem nenhum chamador" não é consulta textual, é consulta sobre arestas. Não existe expressão regular que responda.
flowchart TD
Q["uma pergunta sobre relação entre símbolos"]
Q --> G["consulta ao grafo"]
Q --> R["grep no repositório"]
G --> GA["devolve as arestas: quem chama, o que chama, a quantos saltos"]
R --> RA["devolve as posições onde o nome aparece como texto"]
RA --> RB["abrir cada arquivo e reconstruir a relação lendo"]
RB --> RC["a relação sai da cabeça do modelo, não do repositório"]
O que cada resposta contém
Contagem de bytes só interessa se a resposta menor for também a melhor. Vale abrir as quatro.
Onde eu começo?
Uma chamada, 3.930 bytes, e sai o censo do repositório: quantos nós de cada tipo, quantas arestas de cada tipo, as linguagens com a contagem real de arquivos, os pacotes e os pontos de entrada.
total_nodes: 31130
total_edges: 73526
node_labels: edge_types:
Section 15029 DEFINES 36705
Function 4307 CALLS 7462
File 2890 USAGE 6457
Module 2707 SIMILAR_TO 4539
Route 2574 HANDLES 3888
Variable 2228 TESTS 3062
Class 663 IMPORTS 1881
Method 323 INHERITS 102
Decorator 15 HTTP_CALLS 47
languages: Python 1138, YAML 42, HTML 5, Bash 5, JavaScript 4, CSS 3, TOML 1
entry_points: 20
USAGE com 6.457 contra 7.462 de
CALLS: 46% das referências não têm alvo único
provado, e o grafo declara isso em vez de esconder.
Ferramenta que junta os dois num tipo só de aresta está vendendo
confiança que não tem.
Quem chama isto, e o que ela chama?
A pergunta era sobre solve_dependencies, o miolo da
injeção de dependência do FastAPI. A primeira chamada acha o símbolo e
já devolve a faixa de linhas exata, mais o grau de entrada e de saída:
$ codebase-memory-mcp cli search_graph \
--project fastapi --name-pattern 'solve_dependencies'
total: 2
fastapi.fastapi.dependencies.utils (fastapi/dependencies/utils.py):
solve_dependencies Function 586-731 2 20
fastapi.fastapi.routing._FrontendRouteGroup (fastapi/routing.py):
_solve_dependencies Method 2216-2252 1 7
utils.py chama vinte coisas e é chamada por duas, e que
existe um método homônimo em routing.py que não é ela.
A segunda chamada percorre o grafo em largura, nas duas direções, e devolve em 3.146 bytes os 72 símbolos alcançados até três saltos — agrupados por módulo, com a distância de cada um — mais os 3 chamadores.
flowchart TD
E2["handle"] -->|"salto 2"| E1["_solve_dependencies em routing.py"]
E1 -->|"salto 1"| S["solve_dependencies"]
S -->|"salto 1"| A["get_dependant, request_body_to_args, _solve_generator e outros 16"]
A -->|"salto 2"| B["analyze_param, get_typed_signature, get_cached_model_fields e outros 30"]
B -->|"salto 3"| C["field_annotation_is_scalar, copy_field_info, validate e outros 20"]
O que meu diff afeta?
Aqui a diferença fica mais brutal, e é a pergunta que mais aparece na
prática: mudei uma linha, o que quebra? Acrescentei um comentário dentro
de solve_dependencies e chamei detect_changes.
Ele leu o diff não comitado, mapeou para o símbolo afetado e
devolveu o raio:
changed_files: 1
fastapi/dependencies/utils.py
seed_symbols: 1
impacted_total: 3
impacted:
fastapi.routing._FrontendRouteGroup:
_solve_dependencies Method 1
handle Method 2
fastapi.routing.py:
__file__ File 1
impacted_modules:
fastapi/routing.py 3
git diff equivalente tem 390 bytes
e diz o que mudou — não diz o que depende do que
mudou, que é a única coisa que eu queria saber.
flowchart TD
D["diff: 1 linha em fastapi/dependencies/utils.py"]
D --> S["símbolo semente: solve_dependencies"]
S -->|"salto 1"| A["_solve_dependencies"]
S -->|"salto 1"| F["o arquivo routing.py"]
A -->|"salto 2"| B["handle"]
B --> R(["raio: 3 símbolos, um módulo só"])
utils.py e routing.py inteiros — 295 kB para
descobrir que o estrago cabe num arquivo.
Que função não tem chamador?
Esta é a que muda de categoria. Restrita ao diretório da biblioteca, a resposta inteira tem 43 bytes:
MATCH (f:Function)
WHERE f.file_path STARTS WITH 'fastapi/'
AND NOT EXISTS { (f)<-[:CALLS]-() }
RETURN count(f)
rows: 1 (cols: COUNT(f))
"10"
fastapi/ não têm nenhuma aresta
CALLS entrando. O que sai do subconjunto suportado —
escrita, MERGE, list comprehension — falha com
erro explícito de unsupported, e não devolvendo vazio.
Por que é barato perguntar
Vale desfazer uma confusão comum: o Codebase Memory MCP
não tem LLM dentro. Ele é um motor de análise
estrutural. Quem traduz "quem chama isso?" para
trace_path(direction="inbound") é o agente que você já está
usando — e é por isso que não há chave de API nem modelo a configurar.
flowchart TD
V["você pergunta em português"] --> A["o agente escolhe a tool e monta os argumentos"]
A --> C["o servidor executa a consulta no grafo"]
C --> S["SQLite devolve as linhas"]
S --> A2["o agente lê as linhas e escreve a resposta"]
A2 --> V2["você recebe a cadeia de chamadas em português"]
A consulta é barata porque o trabalho difícil já foi feito na indexação,
em duas passadas. A primeira é tree-sitter, sintática, com as
gramáticas compiladas dentro do binário. A segunda — que o projeto chama
de Hybrid LSP — é uma implementação enxuta em C de algoritmos de
resolução de tipo, e é ela que sabe que
user.profile.display_name() resolve para
Profile.display_name, declarado três módulos adiante.
Existe para Python, TypeScript, PHP, C#, Go, C, C++, Java, Kotlin, Rust
e Perl; as outras linguagens caem na resolução textual.
flowchart TD
F["arquivos do repositório"] --> D["descoberta: .gitignore, .cbmignore, sem symlink"]
D --> TS["passada tree-sitter: definições, chamadas e imports"]
TS --> LSP["passada Hybrid LSP: resolve o alvo por inferência de tipo"]
LSP --> S["SQLite: nós e arestas, no cache local"]
S --> Q(["consulta estrutural"])
S --> A[".codebase-memory/graph.db.zst"]
USAGE e CALLS no censo acima.
Indexei quatro repositórios públicos de tamanhos bem diferentes, na mesma máquina, para ver como esse custo escala:
| Repositório | Linhas .py |
Indexação | Nós | Arestas | Banco |
|---|---|---|---|---|---|
psf/requests | 12.032 | 4,95 s | 1.428 | 5.629 | 8,6 MB |
pallets/flask | 18.345 | 4,35 s | 2.034 | 8.408 | 10,2 MB |
fastapi/fastapi | 112.998 | 8,80 s | 31.130 | 73.526 | 73,9 MB |
django/django | 526.186 | 30,08 s | 55.583 | 345.183 | 269,8 MB |
Meio milhão de linhas de Python em 30 segundos, uma vez, e depois a
pergunta sai em milissegundos. O detalhe honesto está numa coluna que
não coube na tabela: o django teve 51 arquivos com
trecho que o parser não conseguiu ler por inteiro
(parse_partial), contra 3 no flask, 1 no
requests e nenhum no fastapi. O indexador
reporta esses arquivos em vez de fingir cobertura — e é por isso que
afirmação de ausência ainda pede grep.
As três memórias
Chamar tudo isso de "memória" esconde que são três coisas com durabilidades diferentes. A segunda é a que de fato ensina um agente ao seguinte, e é a que quase ninguém usa.
| Memória | O que guarda | Sobrevive a |
|---|---|---|
| o grafo | estrutura derivada do código: símbolos, chamadas, rotas | sessão e reinício da máquina |
o ADR (manage_adr) | o que o código não diz: decisão, trade-off, armadilha | sessão — e é o único que não se regenera |
o artefato .zst | o grafo comprimido, versionado ao lado do fonte | a máquina — atravessa o time |
O grafo é derivado: apagar o cache não perde nada além de tempo de CPU. O ADR não. Ele é um documento em markdown por projeto que um agente escreve e o próximo lê — o recado que quem está saindo deixa para quem está chegando:
## ARCHITECTURE
A resolução de dependências tem um único ponto de entrada:
solve_dependencies (fastapi/dependencies/utils.py:586-731). O único
chamador real é _FrontendRouteGroup._solve_dependencies
(fastapi/routing.py:2216-2252).
## PATTERNS
Não procure a resolução em routing.py: ela delega.
content de
manage_adr(mode='update') e volta inteiro no
mode='get'. As duas consultas da segunda pergunta viram
400 bytes que a próxima sessão lê antes de abrir arquivo.
A terceira memória atravessa pessoas. Indexando com
persistence ligado, o grafo é exportado para
.codebase-memory/graph.db.zst, ao lado do fonte: quem clona
o repositório importa o artefato e roda indexação incremental só do
próprio diff. No FastAPI o artefato ficou em
6,2 MB contra 73,9 MB de banco local, e o
artifact.json ao lado registra o commit exato de
que ele saiu — o que impede alguém de confiar num grafo de três meses
atrás.
sequenceDiagram
autonumber
participant A as sessão 1
participant G as grafo
participant D as ADR
participant B as sessão 2
A->>G: index_repository
G-->>A: 31.130 nós
A->>G: trace_path
A->>D: escreve a conclusão
Note over D: o que o código não diz
B->>D: lê antes de tudo
D-->>B: a conclusão da sessão 1
B->>G: pergunta só o que falta
Instalar e usar
É um executável nativo único, sem runtime de linguagem, sem serviço e sem chave de API. O instalador detecta os agentes já presentes na máquina e escreve a configuração de MCP de cada um — na v0.10.8 ele declara 43 superfícies de cliente:
curl -fsSL https://raw.githubusercontent.com/DeusData/codebase-memory-mcp/main/install.sh | bash
# só o binário, sem mexer na configuração dos agentes:
# ... | bash -s -- --skip-config
Preferindo configurar à mão, é uma entrada de MCP como qualquer outra.
No Claude Code, em ~/.claude.json ou no
.mcp.json do projeto:
{
"mcpServers": {
"codebase-memory-mcp": {
"command": "/home/voce/.local/bin/codebase-memory-mcp",
"args": []
}
}
}
/mcp simplesmente não mostra a entrada.
Depois é reiniciar o agente e dizer "indexe este projeto". Todo tool do MCP também é comando de linha, o que serve para depurar sem um agente no meio e para pendurar indexação num job noturno:
codebase-memory-mcp cli index_repository --repo-path /caminho/do/repo
codebase-memory-mcp cli search_graph --project meu-projeto \
--name-pattern '.*Handler.*' --label Function
codebase-memory-mcp cli trace_path --project meu-projeto \
--function-name solve_dependencies --direction both
codebase-memory-mcp cli detect_changes --project meu-projeto
# o artefato de time NÃO nasce sozinho: persistence é false por default
# index_repository(repo_path="...", persistence=true)
codebase-memory-mcp config set auto_index true # indexa ao abrir a sessão
codebase-memory-mcp --ui=true --port=9749 # grafo em 3D no navegador
get_architecture para se orientar,
search_graph para achar o nome exato,
trace_path para as relações e
get_code_snippet só no fim — quando já se sabe quais são
as 146 linhas que importam.
O que isso custa
Disco. É o custo mais visível: 31.130 nós ocupam
73,9 MB, e o django chega a 269,8 MB. Nesta máquina há
101 projetos indexados e 5,1 GB de cache. Tudo
descartável, nada desprezível.
Frescor. Um grafo de ontem responde com confiança sobre
código que mudou hoje, e essa é a falha mais perigosa da ideia toda —
porque não parece uma falha. O projeto trata com um observador em
segundo plano, com detect_changes para o que ainda não foi
comitado e com check_index_coverage, que lista os arquivos
não cobertos por inteiro. Mas cobertura limpa significa "nenhuma lacuna
registrada", não prova de completude: afirmação de
ausência continua exigindo grep.
Histórico do Git. Se você comitar o artefato, comite com cadência. Ele é reescrito a cada indexação, o Git guarda cada reescrita como um blob novo inteiro, e o próprio projeto documenta um time que chegou a ~6 GB em ~350 commits desse único caminho. Um marco, uma release, um job noturno — não cada salvamento.
Feita a conta, ainda fecha com folga. O maior grafo que eu mantenho indexado tem 167.902 nós e 765.371 arestas; nenhuma janela de contexto comporta aquele repositório, e nunca vai comportar. A pergunta nunca foi se o agente cabe no código — é se ele chega na sessão sabendo onde procurar. É o mesmo que a gente pede de um desenvolvedor novo: não que tenha lido tudo, mas que não comece do zero toda segunda-feira.
notas
-
Versão medida:
codebase-memory-mcp 0.10.8, MIT, em Ubuntu 24.04.5 LTS com Intel Core i7-1355U (12 threads) e 31 GiB de RAM. Repositórios clonados com--depth 1nos commits50113da(fastapi),2b30f62(django),d73fa1c(flask) edae7ef6(requests). Tempo de indexação por/usr/bin/time -f %e; tamanho de banco e de artefato em MB decimais, porstat -c%s. Os números de outras máquinas citados no texto — o kernel do Linux em 3 minutos, os ~6 GB de histórico — são doREADMEdo projeto e da preprint arXiv:2603.27277; não são meus. ↩ -
As quatro medições, para reprodução. Onde eu começo:
cli get_architecture(3.930 B) contrawc -c README.md(22.780 B) mais a listagem defind . -name '*.py'(57.679 B). Quem chama isto:cli search_graph --name-pattern 'solve_dependencies'(382 B) ecli trace_path --direction both --depth 3(3.146 B) contragrep -rn "solve_dependencies" --include='*.py' .(558 B, 8 ocorrências) maiswc -cdefastapi/dependencies/utils.py,fastapi/routing.pyefastapi/dependencies/models.py(302.904 B). O que meu diff afeta:cli detect_changes(544 B) contragit diff(390 B) mais os dois arquivos do raio (295.239 B). Sem chamador: oquery_graphdo texto (43 B). Comparei bytes de saída, não tokens contados com um tokenizer: a conversão de 4 bytes por token é a mesma razão que a ferramenta usa como teto ao dimensionar resposta, e serve para ordem de grandeza, não para dois dígitos de precisão. A janela de 200 mil é a referência do gráfico, não um limite do servidor. O nome do projeto nos blocos está encurtado parafastapi: o derivado do caminho aqui era mais longo, e só o nome muda. ↩