mcp-teste-llm2
# MCP + Ollama — consulta em linguagem natural a tickets
Sistema **somente leitura**: nenhuma tool disponível cria, edita, fecha ou
cancela tickets — só consulta. Qualquer pedido de escrita em linguagem
natural é recusado antes mesmo de chegar ao modelo (`agent/write-policy.js`).
## Arquitetura (visão rápida)
```
Usuário → web/ (frontend estático)
→ agent/server.js (HTTP, sem framework)
→ agent/tickets-routing.js decide a tool E os parâmetros por regex,
SEM depender do LLM pra isso (baixo risco de "alucinação de tool")
→ agent/agent-loop.js chama o Ollama só pra formalizar a chamada da tool
já decidida (o Ollama não escolhe livremente qual tool usar)
→ agent/mcp-client.js (stdio) → src/server.js (servidor MCP, 24 tools)
→ src/tickets-api.js (cliente HTTP da API de tickets)
→ agent/response-formatter.js monta a resposta final em texto
```
## Requisitos
- Node.js 20+
- npm
- [Ollama](https://ollama.com) instalado localmente, com o modelo `qwen2.5:3b`
- Acesso à API interna de tickets (URL base, token, login e app já
provisionados por quem administra o sistema)
## Passo a passo para rodar do zero
### 1. Clonar e instalar dependências
```bash
git clone https://github.com/leomc06/mcp-teste-llm2-tickets.git
cd mcp-teste-llm2-tickets
npm install
```
### 2. Configurar variáveis de ambiente
```bash
cp .env.example .env
```
Edite `.env` e preencha `TICKETS_API_BASE_URL`, `TICKETS_API_TOKEN`,
`TICKETS_API_LOGIN` e `TICKETS_API_APP` com os valores reais da API de
tickets. O `.env` nunca é versionado — confira que ele não aparece em
`git status`.
Todas as demais variáveis do `.env.example` são opcionais (já têm um
default sensato) — a tabela abaixo documenta cada uma:
| Variável | Default | Para que serve |
|---|---|---|
| `AGENT_HOST` | `127.0.0.1` | Endereço em que o backend escuta. **Não mude pra `0.0.0.0`/rede sem colocar autenticação e HTTPS na frente** — ver "Limitações conhecidas" abaixo. |
| `AGENT_PORT` | `3100` | Porta do backend. |
| `AGENT_REQUEST_TIMEOUT_MS` | — | Timeout máximo por requisição ao endpoint de consulta. |
| `AGENT_MAX_TOOL_CALLS` | — | Máximo de chamadas de tool por pergunta. |
| `AGENT_MAX_CONCURRENT_REQUESTS` | — | Quantas consultas podem processar ao mesmo tempo (global, não por usuário). Acima disso, `429 agente_ocupado`. |
| `AGENT_RATE_LIMIT_WINDOW_MS` | `60000` | Janela do limite de requisições por IP. |
| `AGENT_RATE_LIMIT_MAX_REQUESTS` | `30` | Quantas requisições um mesmo IP pode fazer dentro da janela acima antes de `429 limite_de_requisicoes`. |
| `OLLAMA_BASE_URL` | — | URL do Ollama local. |
| `OLLAMA_MODEL` | — | Modelo usado (`qwen2.5:3b`). |
| `OLLAMA_NUM_THREAD` | — | Threads que o Ollama usa pra inferência. |
| `OLLAMA_TIMEOUT_MS` | — | Timeout de chamada ao Ollama. |
| `TICKETS_API_BASE_URL` / `TOKEN` / `LOGIN` / `APP` | — | Credenciais da API de tickets (obrigatórias, sem default). |
| `TICKETS_API_TIMEOUT_MS` | `10000` | Timeout por requisição à API de tickets. |
| `TICKETS_API_METADATA_CACHE_TTL_MS` | `300000` | TTL do cache em memória dos catálogos (status/área/prioridade/canal/departamento/operador) — eles mudam raramente, então repetir a busca a cada pergunta é desperdício. |
| `TICKETS_API_FAN_OUT_CONCURRENCY` | `8` | Teto de chamadas concorrentes nas tools de resumo que contam 1 item de catálogo por vez (ex.: "resumo por operador"). |
### 3. Preparar o Ollama
```bash
sudo systemctl start ollama
ollama pull qwen2.5:3b
```
### 4. Iniciar o backend agente
```bash
npm run start:agent
```
O backend sobe o servidor MCP automaticamente e serve a interface web.
Acesse:
```text
http://127.0.0.1:3100
```
Faça perguntas como:
- "Busque o ticket 4830."
- "Liste os tickets da área de Redes."
- "Resumo dos tickets por status."
- "Quais tickets estão congelados?"
- "Quais áreas de ticket existem?"
- "Compare a carga do Fábio Gali com a do Cesar Augusto de Mello."
- "Compare o ticket 100 com o ticket 200."
### 5. Encerrar
No terminal do backend, `Ctrl+C` (isso também encerra o servidor MCP filho).
```bash
sudo systemctl stop ollama
```
## Rodando via Docker (alternativa ao passo a passo acima)
```bash
cp .env.example .env # preencha TICKETS_API_* como no passo 2 acima
docker compose up -d
docker compose exec ollama ollama pull qwen2.5:3b # só na primeira vez
```
Acesse `http://127.0.0.1:3100`, igual ao modo local. O `docker-compose.yml`
sobe 2 serviços: `agent` (backend + servidor MCP, builda a partir do
`Dockerfile` deste repo) e `ollama` (imagem oficial `ollama/ollama`, modelo
persistido num volume nomeado — não precisa baixar de novo a cada
`docker compose up`).
Detalhes que importam se for mexer nisso:
- `.env` **não é copiado pra imagem** (está no `.dockerignore`) — é
injetado em runtime via `env_file:` no compose. Nunca rebuilde a imagem
com credenciais dentro dela.
- `OLLAMA_BASE_URL` e `AGENT_HOST` do seu `.env` local são sobrescritos
explicitamente no `docker-compose.yml` (o serviço `agent` precisa
alcançar `ollama` pelo nome do serviço, não por `127.0.0.1`, e precisa
escutar em `0.0.0.0` pra a porta publicada funcionar) — se você adicionar
novas variáveis de ambiente no futuro, cheque se alguma delas também
precisa desse tipo de override pra container.
- `run-agent.sh`/`run-mcp.sh` só fazem `source .env` se o arquivo existir
fisicamente — em container, sem esse arquivo, contam com as variáveis já
injetadas pelo `docker run --env-file`/`env_file:` do compose.
- Rodar `docker compose down -v` remove também o volume do modelo do Ollama
(vai precisar baixar de novo). `docker compose down` (sem `-v`) preserva.
## Rodando os testes (opcional)
```bash
npm test
```
Roda `node --check` em todos os arquivos e a suíte `node --test` (roteamento,
formatação, cliente da API de tickets, cliente do Ollama, whitelist do
cliente MCP, rate limiter e sanitização — tudo com mocks, não precisa da API
de tickets real nem do Ollama de pé).
O que a suíte **não** cobre: as 24 tools MCP em si (`src/server.js`) e a
camada HTTP do backend (`agent/server.js`) de ponta a ponta — isso só é
exercitado pelo teste de integração abaixo, contra a stack real.
Com o backend e o Ollama já de pé em outro terminal, dá pra rodar também o
teste de integração ponta a ponta (usa a API de tickets real configurada no
`.env`):
```bash
node --env-file=.env integration-agent.mjs # todos os casos
node --env-file=.env integration-agent.mjs 1 # só o caso 1
```
## Limitações conhecidas
- **Sem autenticação real.** O header `X-User-Id` é só um rótulo de
auditoria (qualquer valor passa, não é validado contra identidade
nenhuma) — quem tiver acesso de rede ao backend tem acesso de leitura a
todos os tickets. Isso é aceitável rodando só em `127.0.0.1`/rede
interna confiável; **não exponha `AGENT_HOST` numa rede não confiável sem
colocar autenticação e HTTPS na frente** (reverse proxy, por exemplo).
Rate limiting por IP existe (`AGENT_RATE_LIMIT_*`), mas não substitui
controle de acesso.
- **Truncamento em consultas muito amplas.** Buscas sem filtro suficiente
(ex.: período antigo sem área/departamento) só varrem os ~1000 tickets
mais recentes antes de desistir — a resposta avisa quando isso acontece
("resultado parcial"), mas o número pode não ser o total exato.
- **Só uma tool por pergunta, com uma exceção deliberada: comparação.**
"Compare a carga do X com a do Y" e "compare o ticket X com o ticket Y"
chamam a mesma tool duas vezes (uma por lado), de forma 100% determinística
(o roteador decide os dois lados, sem passar pelo Ollama). Qualquer outra
combinação (ex.: comparar duas áreas, ou perguntas que misturam tools
diferentes) não é suportada — faça perguntas separadas.
- **Prioridade e cliente/solicitante não são filtráveis no servidor da API
de tickets** — só status, área, departamento e operador são. Isso afeta
o desempenho e a exatidão de consultas amplas por esses dois campos.
TDQS
Scored across 24 tools
Tools are largely distinct by resource and operation, with clear guidance in descriptions (e.g., use buscar_ticket_por_numero for ticket numbers, resumo_* for aggregations). However, the generic listar_tickets overlaps with several specialized listar_tickets_* tools that filter by status, operator, or recency, which could lead to occasional misselection. Descriptions usually resolve the ambiguity, so the set is mostly clear.
All tool names follow a consistent snake_case verb_noun pattern in Portuguese: listar_*, buscar_*, resumo_*, and analisar_*. The structure is predictable and reflects the tool's purpose. No mixed conventions or cryptic names.
With 24 tools, the server is on the heavy side for a ticket system, and several aggregation or listing tools could potentially be consolidated (e.g., resumo_tickets_por_* dimensions). Each tool does have a distinct purpose, but the volume feels borderline excessive. A leaner surface might reduce cognitive load.
The tool set covers reading, listing, searching, and aggregating tickets but lacks any mutation operations: no create, update, delete, assign, transition, or comment tools. For a ticket management domain, this is a significant gap that prevents agents from performing core lifecycle actions. The server appears read-only, which limits its utility as a complete ticket system interface.