veridex-mcp
# 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>
TDQS
Scored across 3 tools
Three tools serve clearly distinct purposes: paying for a company lookup, reading the service's own pricing manifest, and checking API health/demand. There is no realistic way to confuse verify_company_by_cif with either of the free informational tools.
All three use a consistent snake_case verb_noun pattern: verify_company_by_cif, get_veridex_info, health_check. The style is predictable and readable throughout.
Three tools is somewhat thin for a server whose core value is a single paid verification endpoint. However, the informational tools (manifest, health) are coherent supporting pieces rather than padding, so it is defensible but borderline.
The core verification operation is covered, including detailed error taxonomy and payment flow. But there is no batch verification, no historical results, and only one real domain action, which is a notable gap for a registry-data service.