Metadata-Version: 2.5
Name: veridex-mcp
Version: 0.1.0
Summary: MCP server for VERIDEX: verify Spanish companies by CIF, with the x402 payment cycle handled.
Project-URL: Homepage, https://api.veridexia.es
Project-URL: Documentation, https://api.veridexia.es/.well-known/x402
Author-email: VERIDEX <habibberrouaine@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: borme,company-verification,mcp,model-context-protocol,spain,veridex,x402
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx<0.29,>=0.27
Requires-Dist: mcp<2,>=1.9
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest<10,>=8.2; extra == 'dev'
Description-Content-Type: text/markdown

# veridex-mcp

Servidor [MCP](https://modelcontextprotocol.io) (Model Context Protocol) para **VERIDEX**:
verificación de empresas españolas por CIF contra el **BORME**, con puntuación de riesgo
explicable y el ciclo de pago **x402** resuelto paso a paso.

Una vez configurado, cualquier agente (Claude Desktop, Cursor, VS Code, Claude Code…)
puede descubrir y usar la verificación sin que tú escribas una línea de HTTP.

```
Agente  ──MCP (stdio)──▶  veridex-mcp  ──HTTPS──▶  https://api.veridexia.es/v1/verify
                              │
                              └── no firma pagos: entrega el reto x402 y espera
                                  un X-PAYMENT que produzca una wallet tuya
```

---

## Qué es y qué no es

**Es** un cliente MCP fino sobre la API pública de VERIDEX: tres herramientas, tipos
explícitos, errores tipados y el reto de pago x402 decodificado para que el agente sepa
exactamente cuánto cuesta, en qué red, en qué activo y a qué dirección.

**No es** una wallet. Este servidor **no firma, no custodia claves y no envía
transacciones**. No puede pagar por ti — y eso es deliberado: un servidor MCP que pudiera
mover fondos en nombre del usuario sería una wallet sin dueño. Lo que hace es entregar al
agente una petición de pago completa y exacta, y dejar que la settle una wallet que tú
controlas.

Tampoco modifica la API: es un consumidor puro de `api.veridexia.es`.

---

## Las tres herramientas

| Herramienta | Argumentos | Coste | Qué devuelve |
|---|---|---|---|
| `verify_company_by_cif` | `cif` (obligatorio), `x_payment`, `idempotency_key` | **0,20 USDC** (primera llamada devuelve 402) | Datos registrales, situación en el BORME, actos recientes y scoring 0–100 explicable |
| `get_veridex_info` | — | Gratis | Precio, red, activo, dirección de cobro y endpoints, leídos de `/.well-known/x402` |
| `health_check` | — | Gratis | Estado de la API, uptime y demanda de hoy (peticiones y agentes únicos) |

### `verify_company_by_cif`

```jsonc
// Entrada
{ "cif": "A58818501", "x_payment": null, "idempotency_key": null }

// Salida (ok = true)
{
  "ok": true,
  "cif": "A58818501",
  "company": {
    "cif": "A58818501",
    "name": "…",
    "legal_form": "Sociedad Anónima",
    "status": "active",
    "status_label": "Activa sin incidencias",
    "province": "Málaga",
    "incorporation_date": "2015-10-02",
    "last_borme_deposit": "2025-08-09",
    "administrator": "…",
    "share_capital_cents": 4500000,
    "insolvency_published_on": null
  },
  "risk": {
    "value": 15,                    // 0 = más seguro, 100 = más arriesgado
    "band": "low",                  // low | medium | high | critical
    "reasons": ["…"],
    "signals": [{ "code": "status_active", "points": 15, "description": "…" }],
    "confidence": "high",
    "assurance": "high",            // cuánto se SABE, no cuánto de malo es
    "conclusive": true
  },
  "recent_acts": [{ "date": "2026-08-28", "section": "Sección Primera", "description": "…" }],
  "mock": false,
  "provenance": { "source": "prometiam", "fetched_at": "…", "from_cache": false, "confidence": "high" },
  "payment": null,
  "error": null,
  "guidance": ""
}
```

`risk.assurance` es el campo que más se malinterpreta: `assurance: "none"` con
`value: 0` significa *no sabemos nada de esta empresa*, no *esta empresa es segura*. El
servidor lo dice explícitamente en `guidance` cuando ocurre.

---

## Instalación

Requiere **Python 3.10+**.

```bash
pip install -e mcp-server
```

Eso instala el paquete `veridex-mcp` en modo editable junto con sus dependencias
(`mcp>=1.9,<2` y `httpx`), y deja disponible el ejecutable `veridex-mcp`.

Comprueba que arranca:

```bash
veridex-mcp --version
python -m veridex_mcp --version     # equivalente, más portable en Windows
```

> **Windows:** si el host lanza el servidor con un intérprete concreto, usa
> `python -m veridex_mcp` con la ruta completa al Python del entorno virtual. Es la
> forma más fiable de que el proceso correcto arranque.

### Variables de entorno

| Variable | Por defecto | Para qué |
|---|---|---|
| `VERIDEX_API_URL` | `https://api.veridexia.es` | Apuntar a un backend local o a un despliegue propio |
| `VERIDEX_API_KEY` | *(sin definir)* | Reservado para un tier premium futuro. **No es necesario**: el endpoint de pago no pide credenciales, el pago *es* la credencial |
| `VERIDEX_TIMEOUT_SECONDS` | `30` | Timeout de cada petición HTTP |

---

## Configurar los hosts

### Claude Desktop

Edita el fichero de configuración:

- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Linux:** `~/.config/Claude/claude_desktop_config.json`

```json
{
  "mcpServers": {
    "veridex": {
      "command": "python",
      "args": ["-m", "veridex_mcp"],
      "env": {
        "VERIDEX_API_URL": "https://api.veridexia.es",
        "VERIDEX_TIMEOUT_SECONDS": "30"
      }
    }
  }
}
```

Si prefieres el ejecutable instalado en lugar del módulo:

```json
{
  "mcpServers": {
    "veridex": {
      "command": "veridex-mcp",
      "args": [],
      "env": { "VERIDEX_TIMEOUT_SECONDS": "30" }
    }
  }
}
```

Cierra Claude Desktop **por completo** (icono de bandeja incluido) y vuelve a abrirlo. Los
servidores MCP se lanzan al arrancar; el icono de herramientas aparecerá junto al cuadro de
texto. Hay un ejemplo listo para copiar en el fichero `claude_desktop_config.json`, en la
raíz del paquete.

### Cursor

Configuración global en `~/.cursor/mcp.json`, o por proyecto en `.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "veridex": {
      "command": "python",
      "args": ["-m", "veridex_mcp"]
    }
  }
}
```

### VS Code

VS Code 1.102+ usa `.vscode/mcp.json` (la clave es `servers`, no `mcpServers`):

```json
{
  "servers": {
    "veridex": {
      "type": "stdio",
      "command": "python",
      "args": ["-m", "veridex_mcp"]
    }
  }
}
```

También puedes ejecutarlo desde la paleta de comandos con **MCP: Add Server**. Comprueba
con `veridex-mcp --transport streamable-http` y el
[MCP Inspector](https://github.com/modelcontextprotocol/inspector) si quieres ver las
herramientas sin un agente de por medio.

---

## El flujo de pago x402, paso a paso

x402 es un protocolo de pago sobre HTTP: en lugar de una API key, se paga por petición. La
primera llamada a un recurso de pago devuelve **HTTP 402 Payment Required** con un *reto*
que describe exactamente cuánto, dónde y en qué activo. Se settle ese reto con una wallet,
se repite la petición con el resultado en la cabecera `X-PAYMENT`, y el recurso responde con
el contenido.

Con este servidor MCP el flujo tiene **cuatro pasos**:

#### 1. El agente pide el precio (gratis)

Llama a `get_veridex_info`, que lee `https://api.veridexia.es/.well-known/x402`. Obtiene:

```json
{
  "network": "base",
  "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  "asset_symbol": "USDC",
  "asset_decimals": 6,
  "pay_to": "0x2bDc11AEB751EC845aE5fED5bbf7c73BF6D0C4E3",
  "price": "0.20 USDC"
}
```

#### 2. El agente llama a `verify_company_by_cif` sin pago

La API responde `402`:

```http
POST /v1/verify HTTP/1.1
Host: api.veridexia.es
Content-Type: application/json

{"cif": "A58818501"}
```

```http
HTTP/1.1 402 Payment Required
X-PAYMENT-REQUIRED: eyJ4NDAyVmVyc2lvbiI6MSwiYWNjZXB0cyI6W3…   ← reto en base64
Content-Type: application/json

{
  "error": "payment_required",
  "detail": "payment required: retry with the X-PAYMENT header",
  "price_eur_cents": 20,
  "amount_atomic": 200000,
  "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  "network": "base",
  "pay_to": "0x2bDc11AEB751EC845aE5fED5bbf7c73BF6D0C4E3",
  "accepts": [{
    "scheme": "exact",
    "network": "base",
    "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    "payTo": "0x2bDc11AEB751EC845aE5fED5bbf7c73BF6D0C4E3",
    "maxAmountRequired": "200000",
    "resource": "/v1/verify",
    "description": "Verificación de empresa española por CIF",
    "mimeType": "application/json",
    "maxTimeoutSeconds": 300
  }]
}
```

`200000` con 6 decimales son **0,20 USDC**.

**No es un error.** Es la primera mitad del ciclo, y el servidor la devuelve como resultado
normal (`ok: false`) con el campo `payment` completo, no como una excepción:

```json
{
  "ok": false,
  "cif": "A58818501",
  "payment": {
    "x402_version": 1,
    "scheme": "exact",
    "network": "base",
    "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    "pay_to": "0x2bDc11AEB751EC845aE5fED5bbf7c73BF6D0C4E3",
    "amount_atomic": 200000,
    "price_eur_cents": 20,
    "resource": "/v1/verify",
    "max_timeout_seconds": 300,
    "accepts": [/* … */],
    "payment_header": "X-PAYMENT",
    "payment_required_header_value": "eyJ4NDAyVmVyc2lvbiI6MSwiYWNjZXB0cyI6W3…",
    "instructions": "Payment required: 200000 atomic units of 0x8335… on base to 0x2bDc… This MCP server cannot sign or send payments. Have a wallet you control build an x402 payload for this exact challenge, then call verify_company_by_cif again with the same cif and the payload in the 'x_payment' argument (sent as the X-PAYMENT header)."
  },
  "error": { "code": "payment_required", "http_status": 402, "retryable": false }
}
```

Reintentar en bucle **no sirve de nada**: el reto es idéntico en cada intento. Por eso
`retryable` es `false`.

#### 3. Una wallet que tú controlas settle el reto

Aquí es donde entra el dinero, y donde este servidor se aparta a propósito. La wallet
(la de tu usuario, o el proveedor x402 que uses) construye un payload de pago firmado para
**ese reto exacto** — misma red, mismo activo, misma dirección, mismo importe — y lo
serializa en base64.

```jsonc
// Lo que la wallet produce (esquema "exact" sobre Base):
{ "x402Version": 1, "scheme": "exact", "network": "base",
  "payload": { "signature": "0x…", "authorization": { /* … */ } } }
//              ↓  base64
//   "eyJ4NDAyVmVyc2lvbiI6MSwic2NoZW1lIjoiZXhhY3QiLCJuZXR3b3JrIjoiYmFzZSIsInBheWxvYWQiOnsi…"
```

#### 4. El agente repite la llamada con el pago

Misma herramienta, mismo CIF, y el payload base64 en `x_payment`:

```json
{
  "cif": "A58818501",
  "x_payment": "eyJ4NDAyVmVyc2lvbiI6MSwic2NoZW1lIjoiZXhhY3Qi…",
  "idempotency_key": "verif-A58818501-2026-10-04"
}
```

El servidor lo envía como cabecera:

```http
POST /v1/verify HTTP/1.1
Content-Type: application/json
X-PAYMENT: eyJ4NDAyVmVyc2lvbiI6MSwic2NoZW1lIjoiZXhhY3Qi…
Idempotency-Key: verif-A58818501-2026-10-04

{"cif": "A58818501"}
```

Y ahora sí:

```json
{ "ok": true, "company": { "name": "…", "status": "active", "…": "…" },
  "risk": { "value": 15, "band": "low", "…": "…" }, "recent_acts": [/* … */] }
```

**`idempotency_key` merece la pena.** Si la conexión se cae justo después de enviar el pago,
reutilizar la misma clave hace que el reintento devuelva el resultado original en lugar de
cobrar dos veces.

> **Alternativa sin agente de por medio:** el mismo ciclo con `curl`, útil para depurar:
>
> ```bash
> # 2. El reto
> curl -sS -X POST https://api.veridexia.es/v1/verify \
>   -H 'Content-Type: application/json' -d '{"cif":"A58818501"}'
> # 4. Con el pago
> curl -sS -X POST https://api.veridexia.es/v1/verify \
>   -H 'Content-Type: application/json' \
>   -H "X-PAYMENT: $X_PAYMENT" \
>   -d '{"cif":"A58818501"}'
> ```

### Cuándo te cobran y cuándo no

| Situación | ¿Se cobra? |
|---|---|
| Empresa encontrada (`200`) | Sí |
| CIF con formato válido pero sin registro (`404`) | **No** — se resuelve antes de cobrar |
| Fuente de datos caída (`503`) | **No** — el reintento es gratis |
| CIF mal formado (`422`) | **No** — se rechaza antes de ninguna búsqueda |
| Falta el pago o es inválido (`402`) | No (todavía) |

---

## Ejemplo: Claude Desktop verificando una empresa

Con la configuración de arriba y Claude Desktop reiniciado, una conversación real:

> **Tú:** Verifica la empresa con CIF A58818501 y dime si tiene riesgo.

Claude llama a la herramienta y recibe el `402`:

> **Claude:** La verificación cuesta **0,20 USDC** en la red **Base**, pagaderos a
> `0x2bDc11AEB751EC845aE5fED5bbf7c73BF6D0C4E3`. No puedo firmar ni enviar el pago yo mismo:
> este servidor MCP no tiene wallet.
>
> Para completarlo, settle este reto con una wallet tuya y pégame aquí el payload base64
> resultante:
>
> ```json
> { "x402Version": 1, "accepts": [ {
>     "scheme": "exact", "network": "base",
>     "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
>     "payTo": "0x2bDc11AEB751EC845aE5fED5bbf7c73BF6D0C4E3",
>     "maxAmountRequired": "200000", "resource": "/v1/verify"
> } ] }
> ```
>
> Si prefieres, dime y te indico cómo hacerlo desde tu wallet.

> **Tú:** Aquí lo tienes: `eyJ4NDAyVmVyc2lvbiI6MSwic2NoZW1lIjoiZXhhY3QiLCJuZXR3b3JrIjoiYmFzZSIsInBheWxvYWQiOnsi…`

Claude vuelve a llamar a `verify_company_by_cif` pasando ese valor en `x_payment`, recibe el
`200` y responde:

> **Claude:** Empresa verificada.
>
> - **Nombre:** MOCK TECNOLOGIAS SA — *Sociedad Anónima*, Málaga
> - **Situación:** Activa sin incidencias (último depósito en BORME: 2025-08-09)
> - **Constituida:** 2015-10-02 · **Capital:** 45.000 €
> - **Riesgo:** **15/100** (banda *baja*), confianza *alta*, conclusivo
>   - `status_active` (+15): la sociedad figura activa sin incidencias publicadas
> - **Actos recientes:** 2026-08-28, Sección Primera — *Cambio de domicilio social*
> - **Fuente:** prometiam (respuesta en directo, no cacheada)
>
> No hay publicaciones de insolvencia ni señales adversas registradas.

Si el CIF no existiera, Claude no habría pagado nada y lo diría tal cual:

> **Claude:** El CIF `A58818501` tiene formato válido pero no aparece en el BORME ni en
> ninguna de las fuentes configuradas. No se ha realizado ningún cargo. ¿Quieres que
> compruebe si el CIF está bien escrito?

---

## Catálogo de errores

Todos los fallos llegan como resultado normal con `ok: false` y un `error` tipado — nunca
como una excepción, para que el agente pueda leer `error.code`, decidir y actuar en lugar de
ver interrumpido su turno.

| `error.code` | HTTP | ¿Reintentable? | Qué significa |
|---|---|---|---|
| `payment_required` | 402 | No (tal cual) | Falta el pago. Trae `payment` con el reto completo. Reintentar sin cambiar el pago no sirve |
| `not_found` | 404 | No | CIF válido, sin registro. **No se ha cobrado** |
| `source_unavailable` | 503 | **Sí** | La fuente de datos falló. **No se ha cobrado** |
| `invalid_request` | 422 | No | CIF mal formado (una letra, 7 dígitos y un carácter de control, p. ej. `A46103834`) |
| `authentication_error` | 401 / 403 | No | Se rechazó `VERIDEX_API_KEY`. El endpoint de pago no necesita clave: lo más rápido es quitarla |
| `rate_limited` | 429 | **Sí**, esperando | Cuota por llamante agotada |
| `transport_error` | *(ninguno)* | **Sí** | No se pudo alcanzar la API (DNS, TLS, timeout). `http_status` es `null` |
| `upstream_error` | cualquiera | 5xx sí | Respuesta que este cliente no modela; el código original queda en `error.details.api_code` |
| `malformed_response` | 200 | No | El 200 no encaja con el contrato. Trae `error.details.raw_response` |

El estado siempre incluye `guidance`: una frase imperativa con el siguiente paso concreto,
escrita para que la siga un modelo.

---

## Desarrollo

```bash
cd mcp-server
python -m venv .venv
.venv/Scripts/activate          # Windows;  source .venv/bin/activate en Unix
pip install -e ".[dev]"

pytest                          # 101 tests, sin red: todo va por httpx.MockTransport
pytest -v tests/test_server.py
```

Estructura:

```
mcp-server/
├── pyproject.toml
├── README.md
├── LICENSE
├── claude_desktop_config.json
├── src/veridex_mcp/
│   ├── __init__.py      versión del paquete
│   ├── __main__.py      python -m veridex_mcp
│   ├── main.py          argparse, logging a stderr, arranque
│   ├── config.py        Settings desde el entorno
│   ├── models.py        los tipos de cada resultado (→ outputSchema)
│   ├── errors.py        jerarquía de errores tipados
│   ├── client.py        transporte HTTP + decodificación del reto x402
│   └── server.py        las tres herramientas MCP
└── tests/
    ├── conftest.py      API mockeada con payloads reales
    ├── test_client.py   un test por cada respuesta posible de la API
    ├── test_server.py   mapeo de errores + protocolo MCP real
    └── test_main.py     arranque, transporte y la regla de stderr
```

**Todo el logging va a stderr.** Bajo el transporte stdio, stdout *es* el canal JSON-RPC:
un solo `print()` corrompe el framing y el host reporta un error de parseo que no nombra la
causa real. Es la forma más común de que un servidor MCP "conecte pero no haga nada", así
que `main.configure_logging()` es explícito en lugar de delegar en `basicConfig()`.

### Publicar en PyPI

El `pyproject.toml` ya está listo (nombre `veridex-mcp`, licencia MIT, `license-files`):

```bash
python -m build
twine check dist/*
twine upload dist/*
```

---

## Licencia

MIT. El texto completo viaja en el paquete, en el fichero `LICENSE`.

VERIDEX · <https://api.veridexia.es> · manifiesto de descubrimiento:
<https://api.veridexia.es/.well-known/x402>
