Validar RFC, CURP y CFDI desde Claude, sin API keys ni PAC
Tabla de Contenidos
Pídele a un agente de IA que valide un RFC y casi siempre escribirá una regex. Si además calcula el dígito verificador, va a rechazar XAXX010101000, el RFC genérico de público en general, porque ese RFC no satisface su propio dígito. El SAT lo asignó por decreto, y el módulo 11 pide un 4 donde el SAT puso un 0.
Si integras facturación, llevas la contabilidad de un despacho o revisas XML de proveedores, ya sabes cómo acaba eso: el formulario que no deja facturar al público en general, la factura que “no existe” en el SAT aunque la tengas en la mano. Son detalles que no puedes confiarle a la memoria de un modelo. Por eso hice mx-fiscal-mcp-server: un servidor MCP open source que le da a Claude (o a Cursor, o a cualquier cliente MCP) ocho herramientas de solo lectura para validar RFC, CURP, CLABE y NSS con sus algoritmos reales, leer CFDI 4.0, consultar al SAT si una factura sigue vigente y buscar en sus catálogos.
No necesita API keys, ni CSD, ni contrato con un PAC. Aquí va la instalación, qué le puedes pedir y los seis detalles que una implementación hecha a mano suele fallar, incluido uno que yo mismo fallé antes de publicarlo.
Instalación en una línea
El servidor está publicado en npm y en el registro oficial de MCP, así que no hay nada que clonar. En Claude Code:
claude mcp add mx-fiscal -- npx -y mx-fiscal-mcp-server
En Claude Desktop o Cursor, agrégalo a claude_desktop_config.json o a ~/.cursor/mcp.json:
{
"mcpServers": {
"mx-fiscal": {
"command": "npx",
"args": ["-y", "mx-fiscal-mcp-server"]
}
}
}
En Windows usa "command": "cmd" con "args": ["/c", "npx", "-y", "mx-fiscal-mcp-server"]. Pide Node.js 20 o superior.
Está hecho sobre el SDK de MCP v2, así que habla la revisión 2026-07-28 del protocolo y sigue atendiendo, desde el mismo proceso, a los clientes de la era 2025 que usan hoy Claude Desktop, Claude Code y Cursor. Si mantienes un servidor propio, escribí qué cambia al migrar a la 2026-07-28.
Las ocho herramientas
| Herramienta | Qué hace |
|---|---|
validate_rfc | RFC de persona física (13 caracteres) y moral (12): dígito verificador, campos desglosados, fecha de nacimiento o constitución, y si es uno de los genéricos del SAT |
validate_curp | CURP de 18 caracteres: dígito verificador, fecha de nacimiento, sexo, entidad y el marcador de siglo |
validate_clabe | CLABE de 18 dígitos: dígito de control, banco y plaza |
validate_nss | NSS del IMSS de 11 dígitos con su dígito Luhn |
generate_test_data | De 1 a 100 personas físicas o morales ficticias y coherentes: RFC y CURP derivados del mismo nombre y fecha, CLABE de un banco real |
parse_cfdi | XML de CFDI 4.0 a JSON, con cada clave de catálogo etiquetada, los dos RFC validados y los totales comprobados |
cfdi_status | Consulta el servicio público del SAT: si la factura existe, si está cancelada, si es cancelable y el resultado de la validación EFOS (lista del 69-B) |
sat_catalog_lookup | Nueve catálogos (régimen fiscal, uso de CFDI, forma y método de pago, entre otros), por clave o por texto |
Solo cfdi_status sale a la red. Las otras siete nunca salen del proceso.
Qué le puedes pedir
Algunos prompts que funcionan bien, con la forma de lo que devuelven:
- “¿
GODE561231GR8es un RFC válido? Desglósalo.” — si es de persona física o moral, la fecha que lleva codificada y un motivo legible para cada fallo: no un simpleinvalid, sino “el dígito verificador final no coincide con el resultado del módulo 11”. - “Genérame 20 clientes con RFC, CURP y CLABE válidos para mi seeder.” — todos los dígitos cuadran, y el RFC y la CURP de cada registro describen a la misma persona. Si solo necesitas los datos sin agente de por medio, está el generador de datos de prueba.
- “Lee esta factura y dime si sigue vigente en el SAT.” — Claude llama a
parse_cfdipara etiquetarlo todo y comprobar la aritmética, y después acfdi_statuscon los cuatro valores que necesita.
Ese último es el flujo que más tiempo ahorra, sobre todo si revisas XML de proveedores uno por uno, y es el que esconde casi todas las trampas de abajo.
Seis detalles que una regex hace mal
1. El RFC de público en general no satisface su propio dígito verificador
Aplica el módulo 11 a los primeros doce caracteres de XAXX010101000 y el algoritmo pide un 4. En cambio, XEXX010101000, el de residentes en el extranjero, sí lo satisface.
Un validador que solo corre el algoritmo rechaza toda factura al público en general, y por eso tantos formularios de facturación la rechazan. Uno que solo revisa la forma deja pasar errores de dedo. El servidor reporta los dos hechos por separado, is_generic: true y check_digit_satisfied: false, y explica con palabras por qué esa combinación es correcta.
2. La CLABE toma la unidad de cada producto, no la suma de sus cifras
El dígito de control de la CLABE pondera los primeros 17 dígitos con 3, 7, 1, 3, 7, 1… De cada producto se queda la unidad, es decir, el producto módulo 10: un 63 aporta 3. Luego se suma, se toma el módulo 10 y se resta de 10 (si da 10, el dígito es 0).
Una implementación que copia Luhn hace otra cosa: cuando un producto pasa de 9, suma sus cifras, y ese mismo 63 aporta 6 + 3 = 9. Las dos rutas no coinciden: con los 17 dígitos 09000000000000000, el algoritmo de la CLABE da 7 y la versión estilo Luhn da 1. Eso basta para aceptar cuentas mal escritas y rechazar cuentas buenas.
3. El carácter 17 de la CURP marca el siglo
La CURP guarda el año de nacimiento con dos dígitos, así que 31 puede ser 1931 o 2031. RENAPO lo resuelve con el carácter 17: si es dígito, naciste antes de 2000; si es letra, en 2000 o después.
Este es el que fallé yo. La primera versión del servidor leía esa regla al revés y, como la fecha de nacimiento se decodifica justo a través de ese carácter, reportaba un “desacuerdo entre el marcador y la fecha” en cada CURP válida que veía. La suite de tests había capturado el mensaje equivocado como comportamiento esperado. Una revisión independiente lo detectó antes de publicar; el vector de prueba canónico BOXW310820HNERXN09 ahora se decodifica, correctamente, como 20 de agosto de 1931.
4. Las claves de entidad de la CURP son de RENAPO, no del INEGI
Las posiciones 12 y 13 de la CURP codifican la entidad de nacimiento con claves propias de RENAPO: DF es la Ciudad de México, MC el Estado de México y NE nacido en el extranjero. No coinciden ni con las claves de entidad del INEGI ni con ISO 3166-2:MX, así que cruzar contra cualquiera de esas tablas etiqueta mal a la gente sin avisar.
5. La consulta al SAT es exigente con el total
cfdi_status le manda al SAT una expresión armada con cuatro valores: RFC emisor, RFC receptor, total y UUID. El total va con el formato que usan las implementaciones de referencia (nodecfdi y phpcfdi): seis decimales, quitando los ceros finales pero dejando al menos uno. Así, 1160.00 viaja como 1160.0:
?re=TES150312DX2&rr=PELJ900521DK2&tt=1160.0&id=5A7B3C1D-9E2F-4A6B-8C0D-1E2F3A4B5C6D
Un total que no respeta ese formato, como un 1,160.00 tecleado a mano, puede hacer que el SAT conteste No Encontrado de una factura que sí existe. Por eso el servidor revisa los cuatro valores antes de gastar una petición, y recomienda pasarle el XML completo para que los saque él mismo.
En la misma consulta viven dos reglas más. Un RFC con & viaja codificado como &, igual que en la implementación de referencia de nodecfdi. Y un campo ValidacionEFOS vacío no significa que el emisor esté en la lista del 69-B: la documentación del servicio del SAT define los códigos (del 100 al 104 cuando el emisor o algún RFC a cuenta de terceros está listado, 200 y 201 cuando no), pero no el valor vacío, así que el servidor lo reporta como unknown, nunca como “listado”. Desde la versión 1.0.2 separa las dos preguntas: efos_state responde solo por el emisor (100, 101 y 104 son listed; 102, 103, 200 y 201 son not_listed) y efos_third_party_state por los RFC a cuenta de terceros, con el código crudo siempre en validacion_efos. La 1.0.1 leía 102 y 103 como «el emisor está listado», justo lo contrario de lo que dice el SAT.
6. El total incluye los impuestos locales
La comprobación aritmética (subtotal menos descuento, más trasladados, menos retenidos) es como detectas una factura alterada o mal generada. Pero el esquema de CFDI 4.0 define el total sobre impuestos federales y locales, y los locales viven aparte, en el complemento implocal. Una factura de hotel con impuesto estatal al hospedaje parece descuadrada justo por ese impuesto si ignoras el complemento. El servidor lo suma.
Leer no es verificar
parse_cfdi lee el XML: comprobante, emisor, receptor, cada concepto con sus impuestos, los totales de impuestos y el timbre fiscal digital. Etiqueta cada clave de catálogo, valida los dos RFC y comprueba la aritmética.
No verifica el sello digital. Una factura que se lee perfecto puede estar cancelada o ser inventada de principio a fin, y para eso está cfdi_status. Y que el SAT no conteste no vuelve inválida la factura: el servicio no tiene página de estado y se cae (su documentación habla de capacidad para 2 millones de consultas por hora y pide no aumentar el volumen, pero no promete disponibilidad). Cuando falla, la herramienta devuelve available: false con el motivo, que es una afirmación sobre el SAT y nunca sobre el documento, tras un timeout de 10 segundos y un reintento.
La misma honestidad aplica a los identificadores. Que el dígito verificador cuadre dice que la cadena está bien formada y nada más. Solo el SAT puede decir que un RFC está dado de alta, y solo RENAPO que una CURP pertenece a alguien; este servidor no le pregunta a ninguno de los dos, y su redacción nunca da a entender que lo hizo. Hasta los datos ficticios llevan aviso: se generan a partir de nombres comunes, así que una CURP o un teléfono generados pueden coincidir por azar con los de una persona real.
Lo que deliberadamente no hace
No construye ni sella facturas. Esa mitad de la facturación electrónica necesita tu CSD y, para timbrar, un PAC, y ya tiene servidor MCP: mcp-cfdi-mx, en Python, que arma, valida y sella CFDI 4.0 con tu CSD. Este es la mitad de lectura, que no necesita nada, y los dos se complementan.
Auto-hospedarlo con seguridad
Además de stdio, el servidor habla Streamable HTTP sin estado, para uso compartido o remoto:
TRANSPORT=http npx -y mx-fiscal-mcp-server
Es seguro por defecto: escucha en 127.0.0.1 y solo acepta cabeceras Host y Origin de localhost, lo que bloquea ataques de DNS rebinding desde una página web. Para exponerlo detrás de un proxy inverso, define HOST=0.0.0.0, ALLOWED_HOSTS con tu dominio público y MCP_AUTH_TOKEN para autenticación Bearer. No te saltes el token: una instancia abierta le hace consultas al SAT a nombre de cualquiera y puede acabar con tu IP limitada.
El XML de una factura es entrada no confiable, así que un documento que declara un DOCTYPE se rechaza antes de parsearlo (un CFDI nunca lo lleva), y hay topes de tamaño y de número de elementos que acotan la memoria que puede consumir un solo parseo.
La aritmética de los dígitos verificadores vive en mx-identifiers, una librería sin dependencias probada contra los vectores públicos del SAT, Banxico y RENAPO, y el servidor tiene 51 tests unitarios offline más un smoke test que llama a cada herramienta sobre el protocolo real en las dos eras. Si buscas servidores MCP para otras áreas, mantengo una lista de los mejores servidores MCP para SEO, con los míos incluidos.
Preguntas frecuentes
¿El servidor necesita e.firma, CSD o una API key?
No. Todos los algoritmos de dígito verificador y todos los catálogos son públicos, y el servicio de consulta de estado de CFDI del SAT es público y no pide autenticación. El servidor solo lee: nunca sella ni cancela nada, así que no necesita credenciales de ningún tipo.
¿Por qué el SAT dice 'No Encontrado' de una factura que sí existe?
Normalmente porque uno de los cuatro valores de la consulta no coincide con lo que tiene el SAT: un total con otro formato (tiene que ir con seis decimales, quitando los ceros finales pero dejando uno, así que 1160.00 viaja como 1160.0), un separador de miles o un UUID mal copiado. Si le pasas el XML completo a cfdi_status, el servidor saca los cuatro valores por su cuenta.
¿XAXX010101000 es un RFC válido?
Sí. Es el RFC genérico del SAT para operaciones con el público en general, asignado por decreto, y aparece en facturas reales. No satisface el dígito verificador del módulo 11 (el algoritmo pide un 4), así que los validadores sin una lista explícita de genéricos lo rechazan. XEXX010101000, el de residentes en el extranjero, también es genérico y ese sí satisface el algoritmo.
¿Sirve para saber si un proveedor está en la lista del 69-B?
Solo a través de sus facturas: cfdi_status devuelve el campo ValidacionEFOS de cada CFDI que consultas, no busca un RFC suelto. El servidor separa al emisor (efos_state) de los RFC a cuenta de terceros (efos_third_party_state) y deja el campo vacío como desconocido. Aun así, contrasta con el listado del 69-B que publica el SAT antes de tomar una decisión.
¿Los datos de prueba generados pueden ser de una persona real?
Por azar, sí. generate_test_data arma cada registro con nombres mexicanos comunes y una fecha de nacimiento aleatoria, y la CURP se deriva justo de esos campos, así que puede coincidir con la CURP o el teléfono de una persona real. Los datos son para fixtures, seeders y demos: nunca los mandes al SAT ni a un PAC, y nunca los uses en lugar de los datos reales de un cliente.