vaglio-mcp
# Vaglio
> MCP server per la ricerca accademica governata. Vagliare: separare il grano
> dalla pula. Trova, apre, legge — e non mente mai su quale delle tre è successa.
Nasce da un incidente misurato il 2026-07-28: nove agent di ricerca in parallelo
hanno fatto scattare il rate-limit di OpenAlex per **18 ore** (`Retry-After:
64902`) a metà di un workflow. Ogni agent si comportava bene da solo; insieme
erano un attacco. Il backoff per-chiamata non protegge da questo, perché il
budget appartiene alla flotta, non al chiamante.
Ricerca completa: `~/ricerche/topics/metodo-ricerca-accademica/pipeline-fonti-open-access/`.
## Cosa fa che gli altri non fanno
Il prior art esiste ed è buono — [`openags/paper-search-mcp`](https://github.com/openags/paper-search-mcp)
(MIT, 20 fonti, backoff, catena OA-first) copre la meccanica delle fonti. Vaglio
non la riscrive: aggiunge i tre organi che mancano.
| Organo | Cosa fa |
|---|---|
| **Governance** | token bucket condiviso **fra processi** (`fcntl.flock` su stato JSON per host) + cooldown propagato: quando un agent prende un 429, tutti gli altri lo sanno subito invece di riscoprirlo uno per uno |
| **Evidenza** | `read_depth` è **derivato dagli artefatti su disco**, non dichiarato: full-text solo se il corpo è stato scaricato e pesa. Il **tetto del tier è derivato dai metadata della fonte**: una review narrativa non può portare un claim da RCT. Tier A su soli metadata viene **rifiutato**. Tier D è un rifiuto *registrato*, con motivazione obbligatoria |
| **Inscrizione** | il run diventa un record versionato in `~/ricerche`, conforme allo schema dell'archivio, con `sources` popolato da ciò che è stato realmente aperto |
## Il tier non si dichiara: si deriva
Europe PMC e PubMed pubblicano il **tipo MeSH** di ogni lavoro, e Crossref marca
i preprint come `posted-content`. Sono dati della fonte, non giudizi — quindi il
tetto di forza di un claim si legge, non si opina:
| Il record dice | Classe | Tier massimo |
|---|---|---|
| `Randomized Controlled Trial`, `Clinical Trial` | primary-trial | **A** |
| `Meta-Analysis`, `Systematic Review` | synthesis | **A** |
| `Review`, `Letter`, `Case Reports`, `Editorial` | secondary | **B** |
| `posted-content` / arXiv | preprint (non peer-reviewed) | **B** |
| `research-article` senza altro | primary-study | nessun tetto |
| niente | unknown | nessun tetto |
Su una ricerca reale (`binaural beats anxiety`, Europe PMC) la separazione è
netta al primo colpo: un solo RCT ammette tier A, mentre una Letter, una Review
e un Case Report si fermano a B. Chi volesse scrivere "tier A: i binaural beats
riducono l'ansia" citando la review viene fermato dal costruttore, non da un
revisore a valle.
Due precisazioni, perché il meccanismo sia onesto: il tetto **non promuove mai**
nulla — un claim debole su un RCT resta debole; e quando la fonte non dichiara il
tipo, non c'è tetto e il giudizio dell'agent vale come prima.
## Le fonti, e il loro ruolo
I ruoli non sono intercambiabili — è questo che fa funzionare il failover.
| Ruolo | Fonti | Nota |
|---|---|---|
| **FIND** | Crossref → OpenAlex → PubMed → OpenAIRE | Crossref è la spina dorsale: 3 req/s, `polite-array` |
| **OPEN** | Unpaywall | dal DOI alla copia open access legale |
| **READ** | arXiv, Europe PMC, CORE | arXiv per fisica/matematica/DSP; Europe PMC dà il **full text XML senza chiave**, ed è dove si leggono i *metodi* |
Su `429` non si ritenta la stessa fonte: **si cambia fonte per lo stesso scopo**.
Ritentare un host che ha risposto con 18 ore di `Retry-After` è tempo buttato.
Fuori per scelta: **Sci-Hub** (esposizione legale che un portfolio commerciale
non può portare) e i riassuntori mediati da LLM (Elicit, Consensus, SciSpace:
mettono un secondo grado di separazione fra claim e fonte primaria).
## Tool
| Tool | Cosa fa |
|---|---|
| `vaglio_search` | cerca con failover per ruolo; riporta quali fonti hanno rifiutato e perché |
| `vaglio_read` | scarica e **archivia** il corpo, rendendo verificabile la profondità |
| `vaglio_status` | vista di flotta: chi è utilizzabile, chi è in cooldown e per quante ore |
| `vaglio_sources` | catalogo fonti, ruoli, capacità di full text |
| `vaglio_appraise` | assegna un tier a un claim, sotto la disciplina che lo guarda |
| `vaglio_inscribe` | scrive il record in `~/ricerche` e rigenera l'indice |
## Installazione
```bash
cd ~/mcp/vaglio-mcp
python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest # i falsificatori
```
Registrazione come server MCP:
```bash
claude mcp add vaglio -- ~/mcp/vaglio-mcp/.venv/bin/vaglio-mcp
```
Nessuna chiave, nessun account, nessuna dipendenza HTTP di terze parti: il
trasporto è `urllib` di stdlib, di proposito — questo strato non può avere una
dipendenza che decida di fare una richiesta scavalcando il throttle.
## Falsificatori
I test **sono** i falsificatori pre-registrati nel record di ricerca, non un
contorno:
- **F1** — il budget è condiviso: due istanze indipendenti (due processi) non
ottengono `rate` token a testa; un 429 su una è visibile all'altra senza che
abbia mai chiamato; un 429 senza `Retry-After` non vale mai zero.
- **F2** — la profondità non mente: file vuoto ≠ full-text; tier A su soli
metadata è rifiutato; tier D senza motivazione è rifiutato; un run letto
perlopiù ad abstract lo dichiara.
- **F3** — il failover cambia **fonte**: l'host bloccato è provato una volta
sola, mai ritentato; se cadono tutte, il risultato vuoto si spiega invece di
sembrare "nessun risultato".
- **F4** — l'inscrizione è valida: frontmatter conforme allo schema
dell'archivio, slug in kebab-case, full text copiato in `sources/`.
- **F5** — il tetto derivato regge: le etichette reali delle API mappano alla
classe giusta; una review rifiuta il tier A e accetta il B; un RCT ammette A;
un preprint è segnalato come non peer-reviewed e limitato; il silenzio della
fonte non declassa nulla; il tetto non promuove mai un claim debole.
Con una regola in più, di governo e non di codice: `vaglio_inscribe` **rifiuta**
`status: falsified`. Dire in conversazione che qualcosa non regge è dovuto;
inscriverlo nel record come verdetto chiuso è una decisione di chi possiede la
ricerca, non dello strumento.
## Usare i primitivi da soli
I due organi centrali sono agnostici rispetto alla ricerca accademica e si usano
senza toccare l'MCP: servono a qualunque flotta di agent.
```bash
pip install "vaglio-mcp @ git+https://github.com/Alemusica/vaglio-mcp"
```
**Governance** — qualunque API con rate limit, condivisa fra processi:
```python
from vaglio_mcp.core.throttle import Throttle, HostBlocked
t = Throttle() # stato condiviso in ~/.vaglio/throttle
try:
t.acquire("api.example.com") # attende il turno, nel budget della flotta
except HostBlocked as e:
... # l'host e fuori per e.seconds_left: cambia strada
t.report_rate_limited("api.example.com", retry_after=3600) # lo dice a tutti
```
**Evidenza** — tier e profondita di lettura, per ricerca ma anche per due
diligence, FTO, audit normativo:
```python
from vaglio_mcp.core.evidence import Finding, Paper, Tier
Finding(claim="...", tier=Tier.A, paper=Paper(title="...", source="..."))
# TierDiscipline: tier A dichiarato su soli metadata viene rifiutato qui,
# non scoperto a valle.
```
Nessuno dei due importa l'altro, e nessuno dei due sa cosa sia un paper: il
throttle conosce host e secondi, l'evidenza conosce claim e artefatti.
## Stato
Versione 0.2.0.
Candidati all'estrazione verso magazzino (dichiarati, non ancora promossi —
regola dei 3 consumatori): `core/throttle.py` come primitivo di governance
riusabile da qualunque flotta di agent, e `core/evidence.py` come disciplina
tier/profondità agnostica rispetto al dominio.
TDQS
Scored across 6 tools
Each tool has a clearly distinct purpose: vaglio_search finds works, vaglio_read retrieves full text, vaglio_status shows budget/blocked hosts, vaglio_sources catalogues sources, vaglio_appraise attaches evidence tiers, vaglio_inscribe archives runs. No ambiguity.
All tool names follow a consistent 'vaglio_<verb>' pattern with lowercase and underscores. Verbs are descriptive and match the action (search, read, status, sources, appraise, inscribe).
Six tools is well-scoped for the server's purpose of searching, reading, appraising, and archiving bibliographic works. Each tool earns its place without unnecessary overlap.
The tool set covers the full logical workflow: search, retrieve full text, check system status, list sources, appraise evidence, and archive results. No obvious gaps for the intended domain.