Skip to main content
Glama
eduardoantoniojunior

OTRS MCP Server

README.md
# OTRS MCP Server

Servidor [Model Context Protocol][mcp] (MCP) para integração com o [OTRS](https://otrs.org/) (Open Ticket Request System).

Permite que assistentes de IA (Claude Desktop, VS Code, Kiro, agentes Python) criem, consultem, busquem e atualizem tickets no OTRS por uma interface padronizada. Acompanha uma API REST autenticada, painel administrativo em React, auditoria em SQLite e instrumentação OpenTelemetry.

[mcp]: https://modelcontextprotocol.io/introduction

**Versão:** 0.2.0 · **Python:** 3.12+ · **Licença:** Apache-2.0

---

## Sumário

- [O que o servidor oferece](#o-que-o-servidor-oferece)
- [Arquitetura](#arquitetura)
- [Pré-requisitos](#pré-requisitos)
- [Início rápido](#início-rápido)
- [Como usar o MCP](#como-usar-o-mcp)
- [Tools MCP](#tools-mcp)
- [Resources MCP](#resources-mcp)
- [API REST](#api-rest)
- [Configuração](#configuração)
- [API keys](#api-keys)
- [Painel administrativo](#painel-administrativo)
- [Segurança](#segurança)
- [Observabilidade](#observabilidade)
- [Deploy em produção](#deploy-em-produção)
- [Estrutura do projeto](#estrutura-do-projeto)
- [Desenvolvimento](#desenvolvimento)
- [Limitações conhecidas](#limitações-conhecidas)
- [Solução de problemas](#solução-de-problemas)

---

## O que o servidor oferece

Três formas de consumir o mesmo backend OTRS:

| Interface | Para quem | Autenticação |
|---|---|---|
| **MCP** (`streamable-http`) | Agentes de IA e clientes MCP remotos | API key obrigatória no header `Authorization` |
| **MCP** (`stdio`) | Cliente MCP local que sobe o processo | Nenhuma (sem rede; credenciais vêm do ambiente) |
| **API REST** (FastAPI) | Scripts, integrações, o próprio frontend | API key ou JWT |
| **Painel web** (React) | Administradores humanos | JWT (login com usuário e senha) |

Operações OTRS cobertas: `SessionCreate`, `TicketCreate`, `TicketGet` (com ou sem `AllArticles`), `TicketSearch`, `TicketUpdate`, `TicketHistoryGet`.

---

## Arquitetura

```
┌────────────────────────┐      ┌────────────────────────┐
│  Agente IA / cliente   │      │  Navegador (admin)     │
│  MCP                   │      │                        │
└───────────┬────────────┘      └───────────┬────────────┘
            │ Bearer sk-otrs-...            │ Bearer JWT
            ▼                               ▼
┌──────────────────────────────────────────────────────────┐
│        Nginx no host (nginx/mcp.conf) + Certbot          │
│  /otrs/mcp → 8001   /otrs/api/ → 3000   /otrs/ → SPA     │
└──────┬──────────────────┬──────────────────┬─────────────┘
       ▼                  ▼                  ▼
┌────────────┐     ┌────────────┐     ┌────────────┐
│ mcp-server │     │    api     │     │  frontend  │
│  FastMCP   │     │  FastAPI   │     │ React+Nginx│
│ :8001      │     │ :3000      │     │ :80        │
└─────┬──────┘     └─────┬──────┘     └────────────┘
      │                  │
      │                  ▼
      │        ┌──────────────────────┐
      │        │ SQLite (WAL)         │
      │        │ /data/otrs-mcp.db    │
      │        │ admin_users,api_keys,│
      │        │ api_usage,login_audit│
      │        └──────────────────────┘
      ▼                  ▼
┌──────────────────────────────────┐   ┌─────────────────────┐
│   Servidor OTRS                  │   │  otel-collector     │
│   (Generic Interface)            │   │  → Tempo / Mimir    │
└──────────────────────────────────┘   └─────────────────────┘
```

### Serviços do Docker Compose

| Serviço | Imagem / build | Porta publicada | Limites |
|---|---|---|---|
| `api` | `Dockerfile.api` (python:3.12.8-slim) | `127.0.0.1:3000` | 1 CPU / 512M |
| `mcp-server` | `Dockerfile` (python:3.12.8-slim) | `127.0.0.1:8001` | 1 CPU / 512M |
| `frontend` | `frontend/Dockerfile` (build Vite + Nginx) | `127.0.0.1:8081` | 0.5 CPU / 128M |
| `otel-collector` | `otel/opentelemetry-collector-contrib:0.108.0` | `127.0.0.1:4317` e `:4318` | 0.5 CPU / 256M |

Todas as portas ficam em `127.0.0.1`. A exposição pública é feita pelo Nginx do host.

---

## Pré-requisitos

- Docker e Docker Compose
- Servidor OTRS com **Generic Interface** habilitada
- Para produção: Nginx e Certbot no host, domínio apontando para o servidor

### Configurar o webservice no OTRS

1. Vá em **Administração → Web Services**
2. Crie ou edite um webservice expondo: `SessionCreate`, `TicketCreate`, `TicketGet`, `TicketSearch`, `TicketUpdate`, `TicketHistoryGet`
3. Anote a URL: `https://seu-otrs/otrs/nph-genericinterface.pl/Webservice/NomeDoWebservice`
4. Garanta que o usuário configurado tem permissão nas filas usadas

---

## Início rápido

```bash
git clone https://github.com/eduardoantoniojunior/otrs-mcp-server.git
cd otrs-mcp-server
cp .env.example .env
```

Preencha o mínimo no `.env`:

```env
OTRS_BASE_URL=https://seu-otrs/otrs/nph-genericinterface.pl/Webservice/MCPConnector
OTRS_USERNAME=seu-usuario
OTRS_PASSWORD=sua-senha

OTRS_ADMIN_USER=admin
OTRS_ADMIN_PASSWORD=escolha-uma-senha-forte
OTRS_JWT_SECRET=<saída de: python -c "import secrets; print(secrets.token_hex(32))">
```

Suba os containers:

```bash
docker compose up -d --build
docker compose ps
curl -s http://127.0.0.1:3000/api/health   # {"status":"ok"}
```

O usuário admin é criado no primeiro start, somente se ainda não existir nenhum e `OTRS_ADMIN_PASSWORD` estiver definido. Acesse o painel em `http://127.0.0.1:8081` (ou pela URL pública do Nginx), faça login e crie sua primeira API key.

### Rodar sem Docker

```bash
uv sync --extra dev

# API REST em :3000
uv run uvicorn otrs_mcp.api:app --port 3000 --reload

# MCP em HTTP na :8001
$env:OTRS_MCP_TRANSPORT="http"; uv run python -m otrs_mcp.main

# Frontend em :5173
cd frontend; npm ci; npm run dev
```

---

## Como usar o MCP

O servidor fala dois transportes, definidos por `OTRS_MCP_TRANSPORT`:

- `stdio` (padrão) — o cliente sobe o processo local e conversa por pipe. Não precisa de rede nem de API key.
- `http` — Streamable HTTP em `/mcp`, para agentes remotos. **Exige API key válida em toda requisição.**

### Autenticação no transporte HTTP

Requisições sem `Authorization: Bearer sk-otrs-...` são recusadas com `401` antes de qualquer tool executar:

```json
{"error": "invalid_token", "error_description": "Authentication required"}
```

São recusadas as keys inexistentes, revogadas (`active = 0`) e vencidas (`expires_at` no passado). A validação usa a mesma tabela `api_keys` da API REST, e as permissões da key viram escopos:

| Permissão da key | Tools liberadas |
|---|---|
| `read` | `get_ticket`, `get_ticket_articles`, `search_tickets`, `get_ticket_history`, `list_customer_users` e os resources |
| `write` | `create_ticket`, `update_ticket` |
| `admin` | todas |

Uma key só de leitura que tente escrever recebe erro na tool, não na conexão:

```
Permissao 'write' necessaria. A API key possui: read
```

### Claude Desktop / Kiro — remoto (HTTP)

```json
{
  "mcpServers": {
    "otrs": {
      "url": "https://seu-dominio/otrs/mcp",
      "headers": {
        "Authorization": "Bearer sk-otrs-sua-api-key"
      }
    }
  }
}
```

O path `/otrs/mcp` corresponde ao `nginx/mcp.conf` deste repositório. Em domínio dedicado, use `https://seu-dominio/mcp`.

### VS Code

```json
{
  "servers": {
    "otrs": {
      "type": "http",
      "url": "https://seu-dominio/otrs/mcp",
      "headers": {
        "Authorization": "Bearer sk-otrs-sua-api-key"
      }
    }
  }
}
```

### Local via stdio

```json
{
  "mcpServers": {
    "otrs": {
      "command": "uv",
      "args": ["run", "python", "-m", "otrs_mcp.main"],
      "cwd": "/caminho/para/otrs-mcp-server",
      "env": {
        "OTRS_BASE_URL": "https://seu-otrs/otrs/nph-genericinterface.pl/Webservice/MCPConnector",
        "OTRS_USERNAME": "usuario",
        "OTRS_PASSWORD": "senha",
        "OTRS_MCP_TRANSPORT": "stdio"
      }
    }
  }
}
```

### Python SDK

```python
import asyncio
from mcp.client.session import ClientSession
from mcp.client.streamable_http import streamablehttp_client


async def main() -> None:
    headers = {"Authorization": "Bearer sk-otrs-sua-api-key"}
    async with streamablehttp_client(
        "https://seu-dominio/otrs/mcp", headers=headers
    ) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()

            tools = await session.list_tools()
            print([t.name for t in tools.tools])

            result = await session.call_tool(
                "search_tickets",
                arguments={"state": "new", "limit": 5},
            )
            print(result)


asyncio.run(main())
```

### Exemplos de pedidos ao agente

Com o MCP conectado, o modelo resolve pedidos em linguagem natural:

- "Abra um ticket na fila Suporte com prioridade 4 high sobre a impressora do 3º andar"
- "Quais tickets estão em aberto na fila Infraestrutura?"
- "Mostre o histórico do ticket 4821"
- "Mude o ticket 4821 para closed successful"
- "Resuma a conversa do ticket 4821" — usa `get_ticket_articles`
- "O que o cliente disse no ticket 4821?" — `get_ticket_articles` com `sender_type=customer`

Retornos incluem links prontos para a interface do OTRS: `get_ticket`, `update_ticket`, `create_ticket` e `get_ticket_history` retornam `WebURL`; `get_ticket` e `get_ticket_history` também retornam `HistoryWebURL`; `search_tickets` retorna `WebSearchURL` e `TicketWebURLs`.

---

## Tools MCP

| Tool | Parâmetros | Observações |
|---|---|---|
| `create_ticket` | `title`, `body`, `queue?`, `priority?`, `state?`, `customer_user?`, `ticket_type?` | Aplica os defaults de `OTRS_DEFAULT_*`; valida prioridade. Retorna `WebURL` |
| `get_ticket` | `ticket_id`, `include_dynamic_fields?`, `include_extended_data?`, `include_articles?`, `article_limit?`, `article_order?`, `article_sender_type?` | `ticket_id` precisa ser numérico. Retorna `WebURL` e `HistoryWebURL`. Com `include_articles=True`, também `Articles` (whitelist) e `ArticleCount` |
| `get_ticket_articles` | `ticket_id`, `limit?` (default 20), `order?` (`asc`\|`desc`), `sender_type?` | Retorna só a lista de artigos (Subject, Body, SenderType, IsVisibleForCustomer, CreateTime) + `WebURL`/`HistoryWebURL`. Use quando o pedido é sobre conteúdo/conversa do ticket |
| `search_tickets` | `customer_user?`, `customer_id?`, `queue?`, `state?`, `priority?`, `title?`, `limit?`, `sort_by?`, `order_by?` | `title` aceita `*` como curinga (convertido para `%`). Retorna `WebSearchURL` e `TicketWebURLs` |
| `update_ticket` | `ticket_id`, `title?`, `queue?`, `priority?`, `state?`, `customer_user?`, `owner?` | Envia só os campos preenchidos. Retorna `WebURL` |
| `get_ticket_history` | `ticket_id` | Retorna `WebURL` e `HistoryWebURL`. Traz apenas eventos (mudanças de estado, atribuições, `ArticleID`), sem corpo dos artigos — use `get_ticket_articles` para o conteúdo |
| `list_customer_users` | `limit?` (default 200) | Deriva a lista de tickets recentes |

Prioridades aceitas (`src/otrs_mcp/constants.py`): `1 very low`, `2 low`, `3 normal`, `4 high`, `5 very high`.

Campos retornados por artigo (whitelist em `constants.py::ARTICLE_WHITELIST_FIELDS`): `ArticleID`, `Subject`, `Body`, `SenderType`, `ArticleType`, `CommunicationChannel`, `IsVisibleForCustomer`, `CreateTime`, `ChangeTime`, `From`, `To`, `Cc`, `ContentType`, `Charset`, `MimeType`. Campos internos do OTRS (`MessageID`, `InReplyTo`, `References`, etc.) são filtrados.

Cada chamada é registrada em `activity.json` com tool, status, duração e `ticket_id`. Campos chamados `password` são removidos antes de gravar.

---

## Resources MCP

| URI | Conteúdo |
|---|---|
| `otrs://ticket/{ticket_id}` | Ticket completo em JSON (metadados; use a tool `get_ticket` com `include_articles=True` para o corpo) |
| `otrs://ticket/{ticket_id}/articles` | Últimos 20 artigos do ticket em ordem descendente |
| `otrs://ticket/{ticket_id}/history` | Histórico do ticket (eventos, sem corpo) |
| `otrs://search/tickets` | Os 20 tickets mais recentes |

---

## API REST

Base: `https://seu-dominio/otrs/api` (ou `http://127.0.0.1:3000/api` local).

Autenticação por header, exceto no health check:

```
Authorization: Bearer <api-key-ou-jwt>
```

### Público

| Método | Rota | Descrição |
|---|---|---|
| `GET` | `/api/health` | Health check |

### Tickets — API key ou JWT

| Método | Rota | Permissão |
|---|---|---|
| `GET` | `/api/tickets` | `read` |
| `GET` | `/api/tickets/{id}` | `read` |
| `GET` | `/api/tickets/{id}/articles` | `read` |
| `POST` | `/api/tickets` | `write` |
| `PUT` | `/api/tickets/{id}` | `write` |
| `GET` | `/api/tickets/{id}/history` | `read` |
| `GET` | `/api/config` | autenticado |

Filtros de `GET /api/tickets`: `customer_user`, `customer_id`, `queue`, `state`, `priority`, `title`, `limit` (1–200), `sort_by`, `order_by`.

Query params de `GET /api/tickets/{id}`: `include_articles` (bool), `article_limit` (1–200), `article_order` (`asc`|`desc`), `article_sender_type` (ex.: `customer`, `agent`, `system`).

Query params de `GET /api/tickets/{id}/articles`: `limit` (1–200, default 20), `order` (`asc`|`desc`), `sender_type`.

### Atividade — API key ou JWT

| Método | Rota | Permissão |
|---|---|---|
| `GET` | `/api/activity` | autenticado |
| `GET` | `/api/activity/summary` | autenticado |
| `DELETE` | `/api/activity` | `write` |

### Administração — somente JWT

| Método | Rota | Descrição |
|---|---|---|
| `POST` | `/api/admin/login` | Login, devolve JWT |
| `POST` | `/api/admin/refresh` | Renova o JWT sem pedir senha |
| `GET` | `/api/admin/me` | Admin autenticado |
| `POST` `GET` `DELETE` | `/api/admin/users[/{id}]` | CRUD de administradores |
| `POST` `GET` | `/api/admin/keys` | Criar e listar API keys |
| `PATCH` | `/api/admin/keys/{id}/revoke` | Desativar key |
| `DELETE` | `/api/admin/keys/{id}` | Remover key |
| `GET` | `/api/admin/activity` | Auditoria de uso (SQLite) |
| `GET` | `/api/admin/login-audit` | Tentativas de login |
| `GET` | `/api/admin/metrics/daily` | Métricas por dia (`days`, 1–90) |

### Exemplo com cURL

```bash
KEY="sk-otrs-sua-api-key"
BASE="https://seu-dominio/otrs/api"

# Buscar tickets novos
curl -s -H "Authorization: Bearer $KEY" "$BASE/tickets?state=new&limit=5"

# Ler o corpo dos artigos de um ticket
curl -s -H "Authorization: Bearer $KEY" \
  "$BASE/tickets/4821/articles?limit=10&order=desc&sender_type=customer"

# Criar ticket
curl -s -X POST "$BASE/tickets" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Impressora sem toner","body":"3o andar, sala 12","queue":"Suporte","priority":"3 normal"}'

# Fechar ticket
curl -s -X PUT "$BASE/tickets/4821" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"state":"closed successful"}'
```

### Códigos de erro

| Código | Significado |
|---|---|
| `401` | Token ausente, inválido, expirado ou key revogada |
| `403` | Permissão insuficiente para a operação |
| `404` | Ticket não encontrado |
| `422` | Validação falhou (`ticket_id` não numérico, prioridade inválida) |
| `429` | Rate limit da API key, ou lockout de login |
| `502` | Erro na comunicação com o OTRS |
| `503` | OTRS indisponível ou API ainda inicializando |

---

## Configuração

Todas as variáveis usam o prefixo `OTRS_` e são lidas do ambiente (`pydantic-settings`).

### OTRS — obrigatórias

| Variável | Descrição |
|---|---|
| `OTRS_BASE_URL` | URL completa do webservice |
| `OTRS_USERNAME` | Usuário do OTRS |
| `OTRS_PASSWORD` | Senha do OTRS |

Faltando qualquer uma, o processo falha no start com `OTRSValidationError`.

### OTRS — opcionais

| Variável | Padrão | Descrição |
|---|---|---|
| `OTRS_VERIFY_SSL` | `true` | Verificação de certificado |
| `OTRS_TIMEOUT` | `30` | Timeout HTTP (s) |
| `OTRS_DEBUG` | `false` | Log de debug por requisição |
| `OTRS_DEFAULT_QUEUE` | `Raw` | Fila padrão |
| `OTRS_DEFAULT_STATE` | `new` | Estado padrão |
| `OTRS_DEFAULT_PRIORITY` | `3 normal` | Prioridade padrão |
| `OTRS_DEFAULT_TYPE` | vazio | Tipo padrão (omitido se vazio) |
| `OTRS_WEB_BASE_URL` | derivado | Base da interface web, usada nos links `WebURL` |
| `OTRS_VALID_QUEUES` | vazio | Filas do dropdown do painel (separadas por vírgula) |
| `OTRS_VALID_TYPES` | vazio | Tipos do dropdown do painel |

Quando `OTRS_WEB_BASE_URL` não é informado, ele é derivado de `OTRS_BASE_URL` cortando em `/nph-genericinterface.pl`.

### Autenticação

| Variável | Padrão | Descrição |
|---|---|---|
| `OTRS_ENV` | `development` | Com `production`, `OTRS_JWT_SECRET` passa a ser obrigatório |
| `OTRS_JWT_SECRET` | gerado | Segredo HS256. Sem ele em dev, um aleatório é gerado e os tokens morrem a cada restart |
| `OTRS_JWT_EXPIRE_MINUTES` | `480` | Validade do JWT |
| `OTRS_ADMIN_USER` | `admin` | Admin criado no primeiro start |
| `OTRS_ADMIN_PASSWORD` | — | Sem isso, nenhum admin é criado automaticamente |

### MCP, banco e CORS

| Variável | Padrão | Descrição |
|---|---|---|
| `OTRS_MCP_TRANSPORT` | `stdio` | `stdio` ou `http`. Com `http`, a API key passa a ser exigida |
| `OTRS_MCP_HOST` | `0.0.0.0` | Bind do MCP em modo http |
| `OTRS_MCP_PORT` | `8001` | Porta do MCP em modo http |
| `OTRS_MCP_ISSUER_URL` | `https://otrs-mcp.local` | Valor de `issuer_url` exigido pelas AuthSettings do SDK; não há OAuth externo |
| `OTRS_DB_PATH` | `/data/otrs-mcp.db` | Caminho do SQLite |
| `OTRS_ACTIVITY_FILE` | `/data/activity.json` | Log de atividade do MCP |
| `OTRS_ACTIVITY_MAX_EVENTS` | `1000` | Eventos mantidos no JSON |
| `OTRS_CORS_ORIGINS` | `http://localhost:5173,http://localhost:8080` | Origens permitidas |

### Frontend e telemetria

| Variável | Padrão | Descrição |
|---|---|---|
| `VITE_BASE_PATH` | `/` | Subpath do build (ex.: `/otrs/`). Aplicado em build time |
| `VITE_OTEL_ENDPOINT` | vazio | Collector para traces do browser |
| `OTEL_TEMPO_ENDPOINT` | vazio | Destino OTLP do collector |

---

## API keys

Formato: `sk-otrs-` seguido de 64 caracteres hex. A chave é exibida **uma única vez**, na criação; o banco guarda apenas o SHA-256 e um prefixo de 12 caracteres para identificação.

Criar pelo painel: **MCP Tokens → Create Token**. Ou via API:

```bash
curl -s -X POST "$BASE/admin/keys" \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{"name":"Agente Suporte","agent_name":"suporte-bot","permissions":["read","write"],"rate_limit":100,"expires_in_days":90}'
```

| Campo | Regra |
|---|---|
| `permissions` | `read`, `write`, `admin`. `admin` satisfaz qualquer verificação |
| `rate_limit` | Requisições por minuto, 1–10000 |
| `expires_in_days` | 1–365, ou omitido para não expirar |

Uma key é rejeitada se não existir, estiver revogada (`active = 0`) ou vencida.

---

## Painel administrativo

React 19 + Vite 6 + TanStack Query 5 + Tailwind.

| Página | Função |
|---|---|
| **Dashboard** | Métricas de uso, atividade por dia, distribuição por tool, ranking de agentes |
| **MCP Tokens** | Criar, revogar e remover API keys; permissões, rate limit, expiração |
| **Admin Users** | Gerenciar administradores |
| **Audit Log** | Operações registradas em `api_usage`, com agente, key, ticket e duração |
| **Login Audit** | Tentativas de login com IP e user agent |
| **Client MCP Wizard** | Gera a configuração pronta para Claude Desktop, VS Code, Python e cURL |
| **Settings** | Estado da conexão com o OTRS, filas e tipos configurados |

Também há componentes de ticket (`TicketList`, `TicketDetail`, `TicketForm`) para operar tickets pelo painel.

---

## Segurança

| Camada | Implementação |
|---|---|
| **Rede** | Portas dos containers em `127.0.0.1`; exposição via Nginx com TLS (Certbot) |
| **Senhas** | bcrypt via `passlib` para admins |
| **API keys** | SHA-256 no banco, valor bruto nunca persistido |
| **MCP HTTP** | `TokenVerifier` valida a API key em cada requisição; `RequireAuthMiddleware` responde `401` antes de executar tool. Escopo `read`/`write` verificado por tool |
| **JWT** | HS256 com `sub`, `username`, `exp`, `iat`, `jti`, `type` |
| **Produção** | `OTRS_ENV=production` sem `OTRS_JWT_SECRET` aborta o start |
| **Brute-force** | 5 falhas em 15 min por usuário **ou** por IP → `429`, avaliado sobre `login_audit` |
| **Rate limit** | Janela de 60 s por API key sobre `api_usage`; `rate_limit` 0 libera |
| **Permissões** | `require_permission("read"/"write")` em cada rota de ticket |
| **Headers** | `X-Content-Type-Options`, `X-Frame-Options: DENY`, `X-XSS-Protection`, `Referrer-Policy`, `Cache-Control: no-store`; Nginx adiciona `Permissions-Policy` |
| **CORS** | Origens por `OTRS_CORS_ORIGINS`, `allow_headers` restrito a `Authorization` e `Content-Type` |
| **Validação** | `ticket_id` por regex `^\d{1,20}$`, prioridade contra lista fechada, Pydantic com `min_length`/`max_length` |
| **Sanitização** | Erros do OTRS viram `502 "Erro na comunicacao com o OTRS"`; detalhes só no log |
| **Sessão OTRS** | `SessionCreate` protegido por `asyncio.Lock`; requisições seguintes enviam apenas `SessionID` |
| **Auditoria** | Toda operação de ticket grava agente, key, ticket e duração em `api_usage` |
| **Containers** | Usuário não-root `otrs`, multi-stage, imagens pinadas (`python:3.12.8`, `uv:0.5`), limites de CPU e memória |
| **Fail2ban** | Jails em `deploy/fail2ban/` para login, abuso de API e varredura de bots |

Segredos ficam apenas em variáveis de ambiente. `record_activity` e `record_tool_call` descartam campos `password` antes de gravar.

---

## Observabilidade

Backend instrumentado sem alteração de código: os Dockerfiles usam `opentelemetry-instrument` como wrapper, com instrumentações de FastAPI, httpx, SQLite3 e logging.

```
api / mcp-server ──gRPC:4317──▶ otel-collector ──OTLP HTTP──▶ Tempo / Mimir
browser ─────────HTTP:4318────▶ otel-collector
```

Para habilitar o envio, defina no `.env`:

```env
OTEL_TEMPO_ENDPOINT=http://IP-DO-GRAFANA:4318
VITE_OTEL_ENDPOINT=https://seu-dominio/otrs/otel
```

Rebuild (`VITE_OTEL_ENDPOINT` entra no build do frontend):

```bash
docker compose up -d --build
```

Consulta no Grafana Explore (Tempo):

```
{ resource.service.name = "otrs-mcp-api" }
```

Sem `OTEL_EXPORTER_OTLP_ENDPOINT` alcançável, o `opentelemetry-instrument` opera em modo noop. Sem `VITE_OTEL_ENDPOINT`, o frontend não envia traces.

---

## Deploy em produção

### Nginx

`nginx/mcp.conf` vem configurado para domínio compartilhado, servindo este projeto sob o subpath `/otrs/`:

| Rota | Destino |
|---|---|
| `/otrs/mcp` | `127.0.0.1:8001/mcp` (match exato, sem trailing slash) |
| `/otrs/api/` | `127.0.0.1:3000/api/` |
| `/otrs/otel/` | `127.0.0.1:4318/` |
| `/otrs/` | frontend |
| `/` | outro serviço em `127.0.0.1:9090` |

Ajuste `server_name` e, se for usar subpath, defina `VITE_BASE_PATH=/otrs/` antes do build do frontend. Depois:

```bash
sudo cp nginx/mcp.conf /etc/nginx/sites-available/mcp.conf
sudo ln -s /etc/nginx/sites-available/mcp.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d seu-dominio
```

> Confira a porta do frontend antes de recarregar: o Compose publica `127.0.0.1:8081`, e o vhost do repositório aponta para `8080`. Veja [Limitações conhecidas](#limitações-conhecidas).

### Systemd, backup e monitoramento

```bash
sudo cp deploy/otrs-mcp.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now otrs-mcp

chmod +x deploy/deploy.sh deploy/backup.sh deploy/healthcheck.sh
(crontab -l 2>/dev/null; echo "0 3 * * * /opt/otrs-mcp-server/deploy/backup.sh") | crontab -
(crontab -l 2>/dev/null; echo "*/5 * * * * /opt/otrs-mcp-server/deploy/healthcheck.sh") | crontab -

sudo cp deploy/otrs-mcp.logrotate /etc/logrotate.d/otrs-mcp
```

Fail2ban:

```bash
sudo cp deploy/fail2ban/jail.local /etc/fail2ban/jail.local
sudo cp deploy/fail2ban/filter.d/* /etc/fail2ban/filter.d/
sudo systemctl restart fail2ban
```

Atualizações posteriores: `./deploy/deploy.sh --pull`.

---

## Estrutura do projeto

```
otrs-mcp-server/
├── src/otrs_mcp/
│   ├── main.py           # Entry point MCP (stdio / streamable-http)
│   ├── tools.py          # 5 tools MCP
│   ├── resources.py      # 3 resources MCP
│   ├── mcp_auth.py       # TokenVerifier de API key + escopos do MCP
│   ├── api.py            # API REST + middleware de security headers
│   ├── routes/admin.py   # Login, refresh, users, keys, auditoria, métricas
│   ├── auth.py           # JWT, API key, rate limit, permissões
│   ├── database.py       # SQLite WAL: schema e CRUD
│   ├── client.py         # Cliente HTTP do OTRS com sessão e retry
│   ├── config.py         # Configuração via pydantic-settings
│   ├── validation.py     # validate_ticket_id
│   ├── activity.py       # Atividade em JSON
│   ├── constants.py      # Prioridades e estados válidos
│   └── exceptions.py     # Exceções do domínio
├── frontend/             # React 19 + Vite 6 + Tailwind
├── nginx/mcp.conf        # Vhost do host
├── otel/                 # Config do collector
├── deploy/               # systemd, scripts, logrotate, fail2ban
├── tests/unit/           # 41 testes
├── docker-compose.yml
├── Dockerfile            # MCP server
└── Dockerfile.api        # API REST
```

### Tabelas do SQLite

| Tabela | Conteúdo |
|---|---|
| `admin_users` | Administradores e hash bcrypt |
| `api_keys` | Keys com hash, permissões, rate limit, expiração, contador de uso |
| `api_usage` | Auditoria de operações; base do rate limit e das métricas |
| `login_audit` | Tentativas de login; base do lockout de brute-force |

---

## Desenvolvimento

```bash
uv sync --extra dev

uv run pytest tests/unit/ -v
uv run pytest tests/unit/ --cov=src/otrs_mcp --cov-report=term-missing

uv run black src/
uv run isort src/
uv run mypy src/
```

Entry points instalados: `otrs-mcp-server` (MCP) e `otrs-mcp-api` (REST).

---

## Limitações conhecidas

Pontos que valem atenção antes de expor o serviço:

1. **Divergência de porta do frontend.** O Compose publica `127.0.0.1:8081:80`; o vhost faz `proxy_pass` para `127.0.0.1:8080`. Alinhe um dos dois, senão `/otrs/` responde 502.
2. **Rate limit conta operações registradas.** A janela usa as linhas de `api_usage`, gravadas pela API REST após o sucesso da operação. As tools MCP registram atividade em `activity.json`, não em `api_usage`, então o rate limit de uma key usada só via MCP não é acionado.
3. **`usage_count` infla no uso via MCP.** O token é validado em cada requisição HTTP do transporte streamable-http, e uma única sessão MCP gera várias requisições. O contador da key sobe mais rápido do que o número de tools chamadas.
4. **Lockout de login por IP e usuário.** Como a contagem considera o username, tentativas repetidas contra um usuário existente podem bloquear temporariamente o login legítimo dele. O desbloqueio é por tempo (15 min).
5. **SQLite sem criptografia em repouso.** Hashes de senha e de key ficam em `/data/otrs-mcp.db`. Proteja o volume e os backups.

---

## Solução de problemas

| Sintoma | O que verificar |
|---|---|
| Erro de SSL ao falar com o OTRS | `OTRS_VERIFY_SSL=false` para certificado interno |
| Redirect 301 do OTRS | Use a URL HTTPS completa em `OTRS_BASE_URL` |
| Start falha com `OTRSValidationError` | Falta `OTRS_BASE_URL`, `OTRS_USERNAME` ou `OTRS_PASSWORD` |
| Start falha pedindo JWT secret | `OTRS_ENV=production` exige `OTRS_JWT_SECRET` |
| Login sempre inválido no primeiro uso | Nenhum admin criado: defina `OTRS_ADMIN_PASSWORD` e recrie o container |
| `401` na API | Key revogada, expirada ou header ausente |
| `403` na API | Falta `read` ou `write` na key |
| `401` no `/mcp` | Cliente MCP sem `Authorization`, ou key inválida/revogada/vencida |
| `Permissao 'write' necessaria` na tool | A key só tem `read`; crie outra com `write` |
| MCP recusa toda key | O container `mcp-server` precisa do volume `app_data:/data` e de `OTRS_DB_PATH` para ler a tabela `api_keys` |
| `429` na API | Rate limit da key; aumente o valor ou use `0` |
| `429` no login | Lockout de brute-force; espere 15 min e confira o Login Audit |
| `422` em rota de ticket | `ticket_id` precisa ser só dígitos |
| `502` em `/otrs/` | Porta do frontend divergente (8080 vs 8081) |
| Traces ausentes no Grafana | Confira `OTEL_TEMPO_ENDPOINT` e `docker compose logs otel-collector` |
| SPA quebrada em subpath | Rebuild com `VITE_BASE_PATH` correto |

```bash
docker compose logs -f api
docker compose logs -f mcp-server
docker compose logs -f otel-collector
sudo journalctl -u otrs-mcp -f
sudo tail -f /var/log/nginx/mcp-admin-error.log
```

---

## Licença

Apache-2.0

TDQS

B3.3/5.0

Scored across 5 tools

Disambiguation5/5

Each tool targets a distinct operation on tickets: create, retrieve individual details, search, update, and retrieve history. There is no meaningful overlap between get_ticket and get_ticket_history because one returns current state and the other returns the audit trail.

Naming Consistency5/5

All tools follow a consistent verb_noun pattern with snake_case: create_ticket, get_ticket, search_tickets, update_ticket, get_ticket_history. The only minor variation is the plural 'tickets' in search_tickets, which is natural and does not break the pattern.

Tool Count5/5

Five tools is well-scoped for a ticket management server. Each tool represents a core operation without unnecessary redundancy, making the set easy to navigate.

Completeness5/5

The tool surface covers the essential ticket lifecycle: creation, retrieval, search, update, and history lookup. For OTRS, this is a complete and practical set with no obvious dead ends.

Maintenance

ActivityActive
ResponsivenessNo issues