Metadata-Version: 2.5
Name: dedalo
Version: 0.1.0
Summary: Read-only tools and data views for MCP servers
Author-email: Lucas Ribeiro <lucasribeiro.sec@gmail.com>
Keywords: agents,llm,mcp,tools
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: License :: Other/Proprietary License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.11
Requires-Dist: httpx2>=2.10
Requires-Dist: mcp<3,>=2.3
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: fastapi>=0.111; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: twine>=6; extra == 'dev'
Description-Content-Type: text/markdown

# dedalo

Ferramentas de leitura para servidores MCP. Cada app declara quais rotas da própria API um agente pode ler, e o dedalo cuida
do resto: o servidor MCP, o token da pessoa em cada leitura, as respostas grandes em listas paginadas e o resumo de cada consulta.

```bash
pip install dedalo
```

Python ≥ 3.11. Dependências: `mcp` e `httpx2`.

## Como funciona

```
agente (motor) ── MCP ──> servidor do dedalo ── HTTP, com o token da pessoa ──> rotas do app (Orfeu)
```

- **Nada de acesso próprio.** O servidor não tem banco nem credencial de serviço: cada ferramenta lê uma rota do app com o
  token de quem perguntou. Quem autoriza é a rota, com as operações do Orfeu, como para a tela. O agente nunca vê mais do que
  a pessoa veria.
- **Só leitura.** Caminho fixo, só GET (ou um POST que só consulta), só os argumentos declarados chegam à rota, e toda
  ferramenta vai anotada como `readOnlyHint`.
- **Token de cada requisição.** Requisição sem `x-access-token` ou `x-app-token` recebe 401 antes do MCP. O token vale por
  requisição, então o renovado entra na chamada seguinte.
- **Uma leitura por sessão.** As respostas ficam guardadas por sessão MCP e por token: paginar uma lista não refaz a consulta.
- **Listas que cabem.** A resposta da rota vira tabela (`columns` + `rows`) com totais, contagens e `next_offset`, tudo com
  segredos mascarados e datas no horário local. O modelo filtra, ordena e conta sobre a lista inteira.
- **A trilha.** Cada resultado leva no `_meta`, em `dedalo/detail`, uma linha do que a consulta pediu e achou, tirada do
  resultado e não do texto do modelo.

## Declarar as ferramentas

```python
from dedalo import ListView, ReadTool, Toolkit, minutes_since

TOOLS = [
    ReadTool(
        name="list_alerts",
        title="Alertas",
        description="Os alertas abertos.",
        path="/alerts",
        properties={"severity": {"type": "string", "enum": ["alta", "media", "baixa"]}},
        view=ListView(
            rows=lambda data, _joined: data["alerts"],
            columns=("id", "host", "severity", "openedAt", "openMin"),
            count_by=("severity",),
            sort_by="openMin", descending=True,
            derived=(("openMin", minutes_since("openedAt")),),
        ),
    ),
    ReadTool(
        name="get_alert",
        title="Alerta",
        description="Um alerta pelo id.",
        path="/alerts/{alert_id}",
        properties={"alert_id": {"type": "string"}},
        required=("alert_id",),
        path_args=("alert_id",),
    ),
]

toolkit = Toolkit("Monitor", TOOLS, target_arguments=("alert_id", "severity"))
```

Erro de declaração (argumento citado sem estar em `properties`, marcador do caminho sem `path_args`, nome repetido) para a
subida, não a primeira pergunta.

| Campo de `ReadTool` | Para quê |
| --- | --- |
| `properties`, `required`, `defaults` | Os argumentos que o modelo vê, em JSON Schema |
| `path_args` | Argumentos que entram no caminho, validados e codificados |
| `method="POST"`, `body` | Rota de consulta por POST; `body` diz quais argumentos vão no corpo |
| `local` | Argumentos usados só aqui, que não chegam à rota |
| `query_names` | Nome do parâmetro na rota, quando difere do que o modelo usa |
| `paging=Paging(...)` | Rota paginada: as páginas são juntadas antes da visão, até `max_pages` |
| `join_path` | Segunda rota lida junto, entregue à visão como `joined` |
| `view=ListView(...)` | A resposta vira lista com filtros, ordem, contagens e páginas |
| `shape` | Da resposta para o objeto que vai ao modelo, quando não é lista |
| `loader` | Leitura própria, para juntar mais de uma rota |
| `guard` | Conferida antes da consulta: devolve o motivo da recusa, ou `None` |
| `found` | O resumo da trilha para um objeto, quando não é lista |
| `failures` | Mensagem ao modelo por status HTTP, só desta ferramenta |

## Montar o servidor no app

```python
from contextlib import asynccontextmanager

import httpx2
from fastapi import FastAPI
from dedalo import ReadServer

@asynccontextmanager
async def lifespan(app: FastAPI):
    async with mcp_server.lifespan():   # o app montado não roda o próprio lifespan
        yield


app = FastAPI(lifespan=lifespan)
# ... rotas do app em /monitor/server, com o Orfeu ...

# As leituras passam pela própria aplicação, dentro do processo: sem rede e pelos mesmos middlewares do Orfeu.
mcp_server = ReadServer(toolkit, base_url="http://app/monitor/server", transport=httpx2.ASGITransport(app=app))
app.mount("/monitor/mcp", mcp_server.app)   # o endpoint fica em /monitor/mcp/mcp
```

Sem `transport`, `base_url` é o endereço de rede da API do app.

No agente, o servidor entra como qualquer servidor MCP:

```yaml
mcp_servers:
  - name: monitor
    url: ${MONITOR_MCP_URL}
```

## As listas no prompt

O servidor manda em `instructions`, na abertura da sessão, como o modelo usa as listas (`dedalo.LIST_GUIDE`). Um motor que não
use as instruções do servidor pode colocar `LIST_GUIDE` no prompt do agente.

## Testes

```bash
python -m venv .venv
.venv/Scripts/pip install -e ".[dev]"     # no Linux, .venv/bin/
.venv/Scripts/python -m pytest
```
