# Plano de Implementação: Fix MCP v4 Bugs

## Visão Geral

Corrigir 8 bugs em 4 fases: erros estruturados, singletons, deduplicação/paginação, e correções menores. Todas as mudanças são em arquivos Python existentes — nenhum arquivo novo.

---

## Fase 1: Try/except global em `analisar`

Bug mais crítico — a tool principal do MCP retorna stack traces brutas em vez de `{erro, dica}`.

### Tarefas

- [x] Envolver todo o corpo de `analisar()` em try/except com `erro_instrutivo()` [complexo]
  - [x] Adicionar try/except externo após `_start` e `_get_current_user()`
  - [x] Except retorna `erro_instrutivo(str(e), "Verifique o numero do processo e tente novamente.")`
  - [x] Log de erro no except via `get_usage_logger().log_tool_call(success=False)`

### Detalhes Técnicos

**Arquivo**: `tools/analise.py`

Estrutura atual (ERRADA):
```python
async def analisar(...) -> dict[str, Any]:
    _start = time.perf_counter()
    user_id, user_email = _get_current_user()
    client = get_supabase_client()
    tipo_processo = await client.detectar_tipo_processo(...)  # ← pode lançar exceção!
    # ... resto sem try/except ...
```

Estrutura corrigida:
```python
async def analisar(...) -> dict[str, Any]:
    _start = time.perf_counter()
    user_id, user_email = _get_current_user()
    try:
        client = get_supabase_client()
        tipo_processo = await client.detectar_tipo_processo(...)
        # ... resto da lógica ...
    except Exception as e:
        try:
            get_usage_logger().log_tool_call(
                tool_name="analisar",
                duration_ms=ms_since(_start),
                user_id=user_id,
                user_email=user_email,
                success=False,
                error_message=str(e),
                params={"numero_processo": numero_processo},
            )
        except Exception:
            pass
        return erro_instrutivo(
            str(e),
            "Verifique o numero do processo e tente novamente. "
            "Se o erro persistir, tente com model='gemini-flash' ou reduza o escopo com document_ids.",
        )
```

---

## Fase 2: Singletons para `SupabaseClient` e `LLMService`

Evitar criar novas conexões HTTP a cada tool call.

### Tarefas

- [x] Converter `get_supabase_client()` para singleton com cache
- [x] Converter `get_llm_service()` para singleton com cache

### Detalhes Técnicos

**Arquivo**: `services/supabase_client.py`

Atual:
```python
def get_supabase_client(user_id: str | None = None) -> SupabaseClient:
    return SupabaseClient(user_id=user_id)
```

Corrigido:
```python
_instance: SupabaseClient | None = None

def get_supabase_client(user_id: str | None = None) -> SupabaseClient:
    global _instance
    if _instance is None:
        _instance = SupabaseClient(user_id=user_id)
    return _instance
```

**Nota**: `user_id` não é usado nas queries (o client usa `service_key`), então o singleton é seguro. Se no futuro RLS per-user for necessário, trocar para cache por user_id.

**Arquivo**: `services/llm_service.py`

Atual:
```python
def get_llm_service(model: str | None = None) -> LLMService:
    return LLMService(default_model=model or LLMService.DEFAULT_MODEL)
```

Corrigido:
```python
_instance: LLMService | None = None

def get_llm_service(model: str | None = None) -> LLMService:
    global _instance
    if _instance is None:
        _instance = LLMService(default_model=model or LLMService.DEFAULT_MODEL)
    return _instance
```

---

## Fase 3: Deduplicação e paralelismo

Corrigir duplicatas em `listar_processos` e paralelismo falso em `visao_geral_processo`.

### Tarefas

- [x] `listar_processos` — deduplicar processos da tabela `jobs`
- [x] `visao_geral_processo` — envolver `proc_task` em `asyncio.to_thread()` para paralelismo real

### Detalhes Técnicos

**Arquivo**: `tools/navegacao.py` — `listar_processos` (branch sem busca)

Opção: usar subquery com `DISTINCT ON` não é possível via PostgREST. Solução: deduplicar em Python.

```python
# Após obter response.data:
seen = set()
processos = []
for job in response.data or []:
    np = job.get("numero_processo")
    if np in seen:
        continue
    seen.add(np)
    proc = job.get("processos") or {}
    # ... montar dict ...
    processos.append({...})
```

**Nota**: O `total` do `count="exact"` reflete o total de jobs, não de processos únicos. Corrigir:
```python
total = len(seen)  # Não ideal — total real requer query separada
```

Na prática, como fazemos `range(offset, offset + limite - 1)` no Supabase e deduplicamos depois, podemos receber menos itens que o `limite`. Solução pragmática: pedir mais itens ao Supabase (`limite * 2`) e truncar após deduplicação.

Alternativa mais limpa: fazer o `DISTINCT` no Supabase usando uma RPC ou view. Mas como ação rápida, a deduplicação em Python é suficiente.

**Arquivo**: `tools/navegacao.py` — `visao_geral_processo`

Atual:
```python
proc_task = sb.processo_completo(numero_processo)  # bloqueia event loop!
```

Corrigido:
```python
proc_task = asyncio.to_thread(
    lambda: (
        sb.client.table("vw_processo_completo")
        .select("*")
        .eq("numero_processo", numero_processo)
        .maybe_single()
        .execute()
    )
)
```

E depois extrair `.data`:
```python
proc_resp, stats_resp, movs_resp = await asyncio.gather(proc_task, stats_task, movs_task)
processo = proc_resp.data
```

Adicionar fallback para tabela `processos` se view falhar (manter comportamento original de `processo_completo`).

---

## Fase 4: Correções menores

Bugs de menor impacto mas que melhoram a qualidade para deep agents.

### Tarefas

- [x] `_montar_contexto_documentos` — trocar `break` por `continue`
- [x] `stats_documentos` — verificar `total_documentos == 0` e retornar erro
- [x] `visao_geral_processo` — adicionar `id` no select das movimentações

### Detalhes Técnicos

**Arquivo**: `tools/_helpers.py` — `_montar_contexto_documentos`

Linha 112 — trocar:
```python
if chars + len(parte) > max_chars:
    break
```
Por:
```python
if chars + len(parte) > max_chars:
    continue
```

**Arquivo**: `tools/estatisticas.py` — `stats_documentos`

Após `if not dados:`, adicionar verificação:
```python
# dados é um dict retornado pela RPC
if not dados or dados.get("total_documentos", 0) == 0:
    return erro_instrutivo(
        MSG_PROCESSO_NAO_ENCONTRADO,
        "Use listar_processos() para ver processos disponiveis.",
    )
```

**Arquivo**: `tools/navegacao.py` — `visao_geral_processo`

No select das movimentações, adicionar `id`:
```python
.select("id, sequencia, data_hora, tipo_nome, descricao, magistrado_nome")
```

E no loop de processamento, incluir `"id": mov.get("id")`.

---

## Fase 5: Lint, Format, Type Check

### Tarefas

- [x] Rodar `ruff check --fix .`
- [x] Rodar `ruff format .`
- [x] Rodar `mypy .`
- [x] Corrigir erros encontrados

### Detalhes Técnicos

```bash
cd /home/marcos/projetos/server-mcp3
ruff check --fix . && ruff format . && mypy .
```
