Skip to main content
Glama
JoaoAndrade18

MCP Observability Server

README.md
# 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

A4.4/5.0

Scored across 6 tools

Disambiguation5/5

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.

Naming Consistency5/5

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.

Tool Count5/5

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.

Completeness4/5

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.

Maintenance

ActivityStale
ResponsivenessNo issues