Gumi-MCP
by igv8101
README.md
# Gumi-MCP
**Servidor MCP de consulta ao workspace, com política de leitura de código e documentação.**
`server.py` e `tunnel_server.py` oferecem leitura. A entrada opcional
`desktop_tunnel.py` agrega o Desktop-Gumi e inclui execução/escrita autorizadas.
A política de arquivos do leitor não restringe o shell. Consulte
[PERFIS_E_RECUPERACAO.md](PERFIS_E_RECUPERACAO.md) para identificar a entrada,
os limites e as verificações de recuperação.
[](https://github.com/igv8101/gumi-mcp/actions/workflows/testes.yml)


---
## O problema
Conectar um agente de IA ao seu repositório parece simples: aponte um servidor de
filesystem para a pasta e pronto. Só que um projeto pessoal raramente é só código.
Ao lado dele moram dados de uso, exports, mídia, bancos locais — e **leitura via MCP
sai da sua máquina**.
Este servidor nasceu de um projeto pessoal de IA, e a primeira medição foi
desconfortável:
> Dos **8.970** arquivos do workspace, **8.034 eram dados pessoais** e apenas **936
> eram código**. O servidor entregava os 8.970.
O Gumi-MCP existe para resolver isso: o agente enxerga o código, e só o código.
## Como resolve
**Uma política, três camadas, um caminho só.** Nenhuma tool manipula `Path` por conta
própria — todas passam por `security.py`:
```
resolver() o caminho está dentro da raiz? (resolve symlinks e `..` antes)
↓
esta_ignorado() é pasta gerada? (.git, .venv, node_modules…)
↓
motivo_privacidade() é zona de dados? (pasta — data, backups, temp… — ou padrão de
nome: .env, *.local.*, *.db, *.jsonl, *.jks, *-wal…)
↓
extensao_legivel() é formato de texto conhecido? (allowlist, não denylist)
```
Duas decisões que valem explicar:
**Indexar não é servir.** Um `.png` entra no índice porque o metadado é útil; o
conteúdo nunca é lido. Já um caminho dentro de uma pasta privada não entra nem como
metadado — o *nome* de um arquivo de dados já é um dado.
**O que é bloqueado continua aparecendo**, marcado como `bloqueado: "privado"`.
Esconder criaria um mapa mentiroso do repositório; o que não pode é o conteúdo sair.
## O que o agente ganha
| | |
|---|---|
| **Cursor sem perda** | `get_changes_since(cursor)` no modelo do [Watchman](https://facebook.github.io/watchman/docs/file-query.html): "o que mudou desde X", sem perder nem repetir evento. Cursor inválido devolve `fresh_instance` em vez de mentir. |
| **Verificação de integridade** | `verify_integrity()` no modelo de [FIM](https://documentation.wazuh.com/current/user-manual/capabilities/file-integrity/how-it-works.html) (AIDE/Tripwire): filesystem × baseline → `{iguais, modificados, sumidos, novos}`. |
| **Índice que não mente** | Watcher em tempo real **e** scanner de reconciliação periódico — o mesmo par que o Wazuh usa, porque watcher sozinho perde o que acontece enquanto o servidor está desligado. |
| **Busca com contexto** | FTS5 com BM25 devolvendo `{path, linha, trecho}` — não uma lista de caminhos para o agente reler. |
| **O índice não é palavra final** | Toda linha do índice volta a passar pela política **na hora da consulta**, como o [Azure AI Search](https://learn.microsoft.com/en-us/azure/search/search-query-access-control-rbac-enforcement) faz com ACL: retrato envelhece, permissão indexada envelhece junto. 0,42 ms por linha. |
| **Reconciliação em setores** | A varredura anuncia cada projeto assim que ele fica pronto, e a busca espera **só o setor que ela cruza** — o mesmo desenho do `refresh=wait_for` do Elasticsearch. Medido na raiz real: 10 dos 12 setores prontos em 2 s, de uma varredura fria de 41,5 s. |
| **Erros que ensinam** | `ToolError` dizendo o que fazer a seguir, não `"Error executing tool X"`. |
| **Proveniência** | Todo JSON carrega `fonte: workspace_observado` — o consumidor sabe que aquilo é observação, não conclusão. |
19 tools, 2 resources, 3 prompts. Todas com `readOnlyHint` e `openWorldHint=false`.
Duas delas — `search` e `fetch` — são **apelidos** para o Deep Research (ver abaixo):
não abrem caminho novo, chamam a mesma busca e a mesma leitura das outras.
## Verificação
```
320 testes · 101 ataques adversariais bloqueados · 10 usos legítimos intactos
```
**Alvo marcado com `*` é prova fraca.** Um caminho que não existe sob a raiz servida
volta como erro de qualquer jeito — bloqueio de verdade, mas sem a política ter sido
consultada. O relatório separa os dois desde 18/09/2026, porque um número que soma as
duas coisas afirma mais do que mediu.
**E o arnês precisa medir a raiz certa.** Medido em 18/09/2026 (lote O4): sem `env=`,
o SDK do MCP entrega ao servidor-filho apenas doze variáveis de sistema — nenhuma
`GUMI_MCP_*`. O filho subia na raiz padrão enquanto o relatório dizia estar testando
outra, e o `*` de prova fraca, calculado no processo pai, saía trocado. Agora o arnês
passa as `GUMI_MCP_*` adiante e **pergunta ao próprio servidor** que raiz ele serve,
em vez de acreditar na variável que ele mesmo enxerga.
Além dos testes de unidade, `tests/verificacao_adversarial.py` **sobe o servidor de
verdade contra um workspace de verdade** e tenta arrancar dados por todo caminho que
consegui imaginar: `..`, caminho absoluto, maiúsculas (`DATA/`), barra dupla, `.` no
meio, `..` interno, leitura por trecho, por metadado, pelo índice, e pela busca. Mais
sete formas de envenenar argumentos e seis de hostilidade que o servidor precisa
*digerir* sem travar (cursor de outra instância, limites de 999.999).
Segurança que quebra a ferramenta não serve — por isso a suíte também exige que o uso
legítimo continue de pé.
## Instalação
```bash
git clone https://github.com/igv8101/gumi-mcp
cd gumi-mcp
python -m venv .venv && .venv/Scripts/activate # Linux/macOS: source .venv/bin/activate
pip install -r requirements.txt
```
Defina a raiz a servir. **Sem ela o servidor serve apenas a própria pasta** — de
propósito: melhor servir de menos do que servir demais em silêncio.
```bash
GUMI_MCP_ROOT=/caminho/do/seu/projeto
```
Se a raiz for a pasta que guarda **vários** projetos, "estar dentro da raiz"
deixa de ser resposta suficiente: ao lado do que pode ser servido passa a
morar o que não pode de jeito nenhum. Para esse caso existe uma lista
**branca** de primeiro nível — o que não estiver nela fica de fora, inclusive
projeto novo que apareça depois:
```bash
GUMI_MCP_PROJETOS=Projeto-A,Projeto-B
```
Vazia (o padrão), a regra fica desligada. Ligada, ela vale para leitura,
índice e busca; o que ficou de fora continua **aparecendo** nas listagens,
marcado `bloqueado: "fora-do-escopo"` — esconder criaria um mapa mentiroso.
A lista é de **projetos, e projeto é pasta**. Arquivo solto na raiz (um
`CLAUDE.md`, um relatório que alguma automação grava ali) é servido sem
precisar entrar nela: ele nunca foi um projeto não autorizado, só caía fora
por a comparação ser pelo primeiro pedaço do caminho. Isso não afrouxa mais
nada — privacidade, extensão liberada e o sniff de binário continuam por
cima, e conteúdo de pasta não autorizada segue barrado.
### Conectar a um cliente
`scripts/integrar_ia.py` registra o Gumi-MCP e o Desktop-Gumi com orquestração
no Claude Desktop e no Codex (app e CLI), com backup, releitura e `--conferir`.
Veja [INTEGRACAO_IA.md](INTEGRACAO_IA.md) para os provedores, credenciais e limites.
O script anterior `conectar_clientes.py` é legado e não instala a orquestração.
Também é possível configurar somente a leitura com comando+args:
```json
{
"mcpServers": {
"gumi-workspace": {
"command": "/caminho/gumi-mcp/.venv/Scripts/python.exe",
"args": ["/caminho/gumi-mcp/server.py"],
"env": { "GUMI_MCP_ROOT": "/caminho/do/seu/projeto" }
}
}
}
```
> **ChatGPT** tem conexão separada: endpoint HTTPS autenticado ou
> [Secure MCP Tunnel](https://developers.openai.com/plugins/deploy/connect-chatgpt).
> O túnel pode conectar o servidor privado sem abrir uma porta pública.
> A associação depende das credenciais e da disponibilidade na conta.
### Revisão 2.8.0
Varreduras incompletas preservam a baseline; o índice de texto remove conteúdo
que virou binário; históricos voltam a passar pela política ao serem consultados.
Linhas muito longas são divididas por `next_line` e `next_column` (reenvie como
`start_line` e `start_column`). A saúde informa espaço SQLite alocado/reutilizável,
e a trilha de auditoria coordena escrita e rotação entre processos.
Validação desta revisão: **382 testes aprovados**, mais inicialização e chamadas
reais por stdio dos dois servidores. As APIs pagas foram simuladas, sem consumo.
Detalhes e limitações em [REVISAO_2026-09-22.md](REVISAO_2026-09-22.md).
### Deep Research: os apelidos `search` e `fetch`
Em Developer Mode o ChatGPT chama qualquer tool pelo nome que ela tiver. O **Deep
Research** não: ele procura duas tools chamadas exatamente `search` e `fetch`, com
forma de resposta fixa — `search(query)` devolve `{"results": [{id, title, url}]}` e
`fetch(id)` devolve `{id, title, text, url, metadata}`, como `structuredContent` e
como JSON no `content`.
As duas existem aqui como **apelido, não como caminho novo**: `search` chama o mesmo
miolo de `search_project` e `fetch` passa pelo mesmo portão de `read_file`. Tudo que
uma bloqueia, a outra bloqueia — com a mesma frase, e há teste exigindo isso. A
verificação adversarial espelha no `fetch` toda tentativa feita contra o `read_file`.
Duas escolhas que valem saber:
- **A URL de citação é `gumi://workspace/arquivo/<caminho>`, nunca `file://`.** Caminho
absoluto carrega o nome de usuário e o desenho das pastas da máquina, e a citação é
justamente o que sai dela.
- **Documento acima do teto de resposta sai cortado, dizendo que foi**
(`metadata.truncado`, `bytes_totais`, `bytes_servidos`). O contrato do Deep Research
não tem paginação; recusar o fim do documento perderia também o começo. Quem precisa
do resto usa `read_file_excerpt`.
Quem puder escolher a ferramenta, prefira `search_project`: ela devolve também linha,
trecho e paginação.
### Ajustar a política ao seu projeto
Os padrões são conservadores. Para acrescentar as pastas do seu projeto sem
versioná-las, crie um `privado.local.json` (já ignorado pelo git):
```json
{ "diretorios": ["minha_pasta_de_dados"], "prefixos": ["_rascunho"] }
```
Ele **soma** aos padrões de `config.py` — nunca afrouxa. A única exceção é a
chave `"projetos"`, que alimenta a lista branca acima: como lista branca
autoriza em vez de bloquear, acrescentar um nome ali **abre** aquele projeto.
Está no mesmo arquivo porque é ele que descreve a máquina de quem usa, e não
vai para o git.
## Configuração
| Variável | Padrão | O que faz |
|---|---|---|
| `GUMI_MCP_ROOT` | a própria pasta | raiz servida |
| `GUMI_MCP_PROJETOS` | vazio (sem restrição) | lista branca de projetos de primeiro nível, separados por vírgula |
| `GUMI_MCP_DATABASE` | `data/gumi_state.db` | índice SQLite |
| `GUMI_MCP_LOGS` | `logs/` | log e trilha de auditoria |
| `GUMI_MCP_SCAN_HOURS` | `6` | intervalo do scanner de reconciliação |
| `GUMI_MCP_ORIGEM` | `desconhecida` | rótulo de quem subiu este processo; vai em cada linha da trilha |
O servidor escreve **apenas** no próprio banco e nos próprios logs. Nada no
workspace é criado, alterado, movido ou executado — nunca.
## Quem leu o quê
A trilha (`logs/auditoria.jsonl`) guarda **uma linha JSON por chamada**: quando, de
onde (`origem`), qual tool, quais argumentos, quantos bytes saíram (`bytes_resposta`)
e se foi negada. Nunca o conteúdo servido — só o tamanho dele.
`origem` vem de `GUMI_MCP_ORIGEM` e é declarada por quem sobe o processo: o servidor
fala stdio, então cada cliente sobe o seu. É o que permite responder *"o que o GPT leu
do meu computador?"* com um arquivo em vez de uma lembrança —
`get_audit_trail(origem="gpt")` devolve só as leituras daquele lado.
Duas honestidades sobre ela. É **declarada, não provada**: separa clientes que se
identificam, e não é defesa contra um cliente que minta. E **não autoriza nada** — a
política de `security.py` não olha para a origem, e tem teste prendendo isso, porque no
dia em que a origem virasse permissão, mentir nela viraria ataque. Quem não se declara
entra como `desconhecida`; carimbar `claude` por padrão seria inventar procedência.
## Notas de implementação
Três coisas que só apareceram construindo, e que valem para qualquer servidor MCP:
**Em stdio, `stdout` pertence ao protocolo.** Um `print()` de debug derruba a conexão
JSON-RPC. Todo log aqui vai para arquivo ou `stderr`.
**Só `ToolError` chega ao cliente.** Qualquer outra exceção vira
`"Error executing tool X"` genérico, e o modelo fica sem saber se errou o caminho, se
o arquivo é grande demais ou se foi bloqueado.
**Watcher em tempo real não basta.** Se o servidor sobe e desce a cada consulta, ele
não vê nada nos intervalos — e o scanner da subida grava *depois* que a primeira
resposta foi montada, o que faz a percepção chegar com uma consulta de atraso. A
correção é perguntar duas vezes na mesma sessão, usando o cursor da primeira.
## Licença
MIT — veja [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues