Skip to main content
Glama
README.md
# doi2ris MCP

Servidor MCP que hace de intermediario entre Claude y las APIs bibliográficas.

## Por qué existe

El entorno de claude.ai no tiene salida a internet. Un script que corra ahí no
puede consultar Crossref ni OpenAlex. Este servidor corre en tu VPS, que sí
tiene red, y Claude le habla a él.

```
   ┌──────────────────── entorno de Claude (sin red) ─────────────────────┐
   │                                                                      │
   │   referencias.txt                                                    │
   │        │                                                             │
   │        ▼                                                             │
   │   doi2ris.py plan ──────────────────► consultas.json                 │
   │                                            │                         │
   │                                            ▼                         │
   │                                    tool `resolver`                   │
   └────────────────────────────────────────────┼─────────────────────────┘
                                                │  HTTPS
                                                ▼
                    ┌─────────── tu VPS: mcpdoi.systempiura.com ──────────┐
                    │                                                     │
                    │   nginx (TLS · token en la URL · limit_req)         │
                    │        │                                            │
                    │        ▼                                            │
                    │   server.py ── caché sqlite ──► ¿ya lo sé? ──► sí ──┼──┐
                    │        │                                            │  │
                    │        ▼ no                                         │  │
                    │   token bucket (auto-ajustado) ──► normalizar       │  │
                    └────────┼────────────────────────────────────────────┘  │
                             ▼                                               │
                    api.crossref.org · api.openalex.org                      │
                                                                             │
   ┌─────────────────────────────────────────────────────────────────────────┘
   │  respuestas.json  (registros ya normalizados, ~1KB c/u)
   ▼
   doi2ris.py resolve ──► ¿faltan búsquedas por título?
        │                      │
        │ no                   └── sí ──► consultas-2.json ──► (otra ronda)
        ▼
   referencias.ris  +  reporte.md
```

## Estrategia contra el bloqueo de Crossref

Crossref no cobra ni pide clave, pero estrangula y termina bloqueando por IP a
quien abusa. Como aquí **todos los usuarios comparten la IP de la VPS**, un solo
usuario descuidado los tumbaría a todos. Cinco capas, de más a menos efectiva:

1. **Caché compartida en disco (30 días).** Es la defensa principal. Vive en un
   volumen Docker y sobrevive a los redeploys. Una referencia que ya pidió otro
   usuario no genera ninguna petición. En una comunidad que cita la misma
   literatura, el ahorro es enorme.

2. **Pool cortés.** `DOI2RIS_MAILTO` mete tu correo en el User-Agent y en cada
   petición. Crossref te mueve al pool cortés: mejor latencia, límites más
   generosos, y **te escriben antes de bloquearte** en vez de cortarte en seco.
   Sin esto caés al pool anónimo, que es el primero en ser estrangulado.

3. **Token bucket que se auto-ajusta.** El servidor lee las cabeceras
   `X-Rate-Limit-Limit` / `X-Rate-Limit-Interval` que Crossref devuelve en cada
   respuesta y se ajusta al 70% de lo que el propio Crossref declara. Nunca pide
   más rápido de lo permitido, aunque Crossref cambie sus límites.

4. **Backoff que obedece.** Ante un 429 o 503 respeta `Retry-After` al pie de la
   letra y además **reduce a la mitad su propio ritmo** de forma permanente. No
   reintenta los 404: un DOI que no existe es una respuesta, no un fallo.

5. **Menos peticiones desde el diseño.** Deduplicación dentro del lote,
   single-flight (dos usuarios pidiendo lo mismo a la vez generan una sola
   petición upstream), y el esquema de dos rondas: las búsquedas por título solo
   se piden para las referencias cuyo DOI falló, no para todas.

El token en nginx y el `limit_req` son la capa de arriba: impiden que un tercero
convierta tu servidor en un scraper y se lleve puesta tu IP.

## Desplegar

```bash
git clone <tu-repo> && cd mcp-doi2ris
cp .env.example .env
nano .env                      # poné tu DOI2RIS_MAILTO — no lo dejes vacío
docker compose up -d --build
curl -sN -X POST localhost:8080/mcp \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'
```

Si eso devuelve un `result`, el contenedor está bien. Después viene nginx, y va
en **dos fases**: primero HTTP, y que certbot añada el TLS él mismo.

**Fase 1 — nginx en el puerto 80**

```bash
openssl rand -hex 24                                    # tu token
cp nginx.conf.example /etc/nginx/sites-available/mcpdoi.systempiura.com
nano /etc/nginx/sites-available/mcpdoi.systempiura.com  # pegá el token
ln -s /etc/nginx/sites-available/mcpdoi.systempiura.com /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
```

Acordate de la línea `limit_req_zone` en el bloque `http { }` de
`/etc/nginx/nginx.conf`, o `nginx -t` falla.

**Fase 2 — certbot pone el TLS**

```bash
certbot --nginx -d mcpdoi.systempiura.com
```

Certbot **edita ese mismo archivo**: te añade el bloque `listen 443 ssl`, los
`ssl_certificate`, los parámetros SSL y el redirect de 80 a 443. No escribas nada
de eso a mano — si ya hay un bloque 443 escrito por vos, certbot se confunde o
duplica directivas. Y como `ssl_certificate` apuntando a un archivo inexistente
impide que nginx arranque, escribirlo antes de tener el certificado te deja el
servidor caído.

Comprobá que la renovación automática quedó puesta:

```bash
certbot renew --dry-run
```

**Fase 3 — probá desde fuera**

```bash
curl -sN -X POST https://mcpdoi.systempiura.com/t/<TOKEN>/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'
```

Si responde `Invalid Host header`, te falta el dominio en `DOI2RIS_ALLOWED_HOSTS`
del `.env`. Si da 404, el token de la URL no coincide con el de nginx.

La URL que repartís es:

```
https://mcpdoi.systempiura.com/t/<TOKEN>/mcp
```

## Operación

- Logs: `docker compose logs -f`
- Salud: la tool `estado` reporta el status de ambas APIs, los aciertos de caché
  y el ritmo actual hacia Crossref. Si `rate_crossref` bajó mucho, es que Crossref
  te estuvo frenando.
- Vaciar la caché: `docker compose down && docker volume rm mcp-doi2ris_doi2ris-cache`
- Rotar el token: cambiás la línea en nginx, `systemctl reload nginx`, y repartís
  la URL nueva. Los conectores viejos empiezan a recibir 404.