MCP Observability Server
# POC — MCP server + client + REST API (observabilidade)
Stack completo e executável: uma API REST de observabilidade, um servidor MCP
que a expõe como ferramentas, e um cliente MCP **sem LLM** que roda um roteiro
de investigação real.
O dataset é sintético mas tem uma história plantada: `checkout-api v2.4.0`
subiu há 3 horas e quebrou o serviço. A ferramenta de correlação precisa
encontrar isso sozinha — e encontra.
> Para o **porquê** de cada decisão de design, veja
> [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md).
## Rodar
Requer [uv](https://docs.astral.sh/uv/) e Python 3.14+.
```bash
git clone https://github.com/JoaoAndrade18/mcp-server-client-poc.git
cd mcp-server-client-poc
uv sync
./run.sh # sobe tudo, roda o cenário, derruba
uv run pytest -q # 18 testes
```
## Arquitetura
```
cliente MCP ──MCP/streamable-http──► servidor MCP ──HTTP+Bearer──► API REST
(sem LLM) :8765/mcp (adapter) :8081 (dados)
│
/metrics /healthz
```
As três camadas são separadas de propósito:
- **A API REST (`api.py`) não sabe o que é MCP.** É o "sistema que já existe"
na sua empresa. O servidor MCP é um adaptador na frente dela, não uma
reescrita.
- **O servidor MCP (`mcp_server.py`) não tem dados.** Traduz protocolo e
guarda o token da API. Quem fala MCP nunca vê essa credencial.
- **O cliente (`client.py`) não tem modelo.** Tudo que um LLM faria
dinamicamente, ele faz por script. Se quebrar, o problema é seu servidor —
não o raciocínio do modelo. É o jeito de isolar as duas coisas.
## O que o POC demonstra
| Área | Onde |
|---|---|
| Tools com output estruturado (Pydantic) | `mcp_server.py` — 6 tools |
| Annotations (`readOnlyHint`, `destructiveHint`) | idem — só `open_incident` escreve |
| Resources (estáticos + template) | `obs://services`, `obs://service/{name}/runbook` |
| Prompts (workflow reutilizável) | `investigate_service` |
| Auth por token + escopos | `StaticTokenVerifier`, `_require_scope` |
| Métricas por tool (calls/erros/latência) | `GET :8765/metrics` |
| Health check com upstream | `GET :8765/healthz` |
| Erros acionáveis | `query_logs("does-not-exist")` |
## Tools
| Tool | Tipo | O que faz |
|---|---|---|
| `list_services` | read | Catálogo com time, tier e SLOs |
| `query_logs` | read | Busca por serviço, nível, substring, janela |
| `get_metrics` | read | Resume 1 métrica + veredito de SLO |
| `get_deploys` | read | Histórico de deploys |
| `correlate_deploy_with_errors` | read | **Antes/depois do último deploy + veredito + confiança** |
| `open_incident` | **write** | Abre incidente (exige escopo `incidents:write`) |
`correlate_deploy_with_errors` é o ponto principal do design: as tools são
**moldadas por tarefa, não por endpoint**. Ela responde "por que X quebrou?"
numa chamada só, em vez de obrigar o chamador a costurar
`get_deploys` + 2×`get_metrics` + `query_logs` e fazer a aritmética sozinho.
## Rodar as partes separadas
```bash
# API REST isolada (docs em /docs)
uv run uvicorn obs.api:app --port 8081
# Servidor MCP — HTTP (produção)
uv run python -m obs.mcp_server
# Servidor MCP — stdio (Claude Desktop, IDEs)
MCP_TRANSPORT=stdio uv run python -m obs.mcp_server
# Inspector oficial
uv run mcp dev src/obs/mcp_server.py
```
## Modo autenticado
Por padrão a auth vem **desligada** para o demo rodar sem fricção. Para ligar:
```bash
MCP_REQUIRE_AUTH=1 uv run python -m obs.mcp_server
MCP_TOKEN=mcp_oncall_token uv run python -m obs.client
```
Comportamento verificado:
| Token | `list_services` | `open_incident` |
|---|---|---|
| nenhum | HTTP 401 | HTTP 401 |
| inválido | HTTP 401 | HTTP 401 |
| `mcp_readonly_token` (`obs:read`) | ✅ | ❌ bloqueado no escopo |
| `mcp_oncall_token` (+`incidents:write`) | ✅ | ✅ |
Os tokens estáticos são só do POC. Em produção, `StaticTokenVerifier` vira um
verificador de JWT contra o JWKS do seu IdP — o formato do retorno
(`AccessToken` com escopos) é o mesmo.
## Nota sobre o `/metrics`
O p95 do servidor foi o que revelou um bug real durante a construção deste
POC: `correlate_deploy_with_errors` reportava **p95 abaixo da média**, o que é
impossível. A causa era `int(n * 0.95) - 1`, que com n=2 dá índice 0 e retorna
o mínimo. Está corrigido em `percentile()` e travado por teste.
É o argumento para instrumentar o servidor MCP desde o dia 1: sem o `/metrics`
o bug teria passado.
## Estrutura
```
src/obs/
data.py dataset determinístico (seed fixa, incidente plantado)
api.py API REST — FastAPI, bearer auth, /metrics
mcp_server.py servidor MCP — tools, resources, prompts, auth, métricas
client.py cliente MCP — sem LLM, roteiro fixo
tests/test_poc.py
run.sh
```
TDQS
Scored across 6 tools
Each tool has a clearly distinct purpose: service discovery, log search, metric retrieval, deploy history, deploy-error correlation, and incident creation. There is no overlap between these functions, and the correlator tool explicitly bundles several lower-level operations, reducing potential confusion.
All tool names follow a consistent verb_noun pattern in lowercase snake_case (list_services, query_logs, get_metrics, get_deploys, correlate_deploy_with_errors, open_incident). The verbs are specific to each action, and the naming style is uniform across the set.
Six tools is well within the ideal range for a focused observability server. Each tool earns its place by covering a distinct aspect of monitoring and operations, without unnecessary bloat or missing essential capabilities.
The set covers the core observability workflow: discovering services, inspecting logs, fetching metrics with SLO status, reviewing deploys, and opening incidents. A minor gap is the lack of incident management features beyond creation (e.g., listing or updating incidents), but this is secondary to the server's primary monitoring purpose.