doi2ris
by SARAScodigos
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.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues