mx-fiscal-mcp-server
Server Configuration
Describes the environment variables required to run the server.
| Name | Required | Description | Default |
|---|---|---|---|
| HOST | No | Bind address for the HTTP transport; use `0.0.0.0` to accept remote connections. The Docker image sets `HOST=0.0.0.0`. | 127.0.0.1 |
| PORT | No | Listening port for the HTTP transport. | 3000 |
| TRANSPORT | No | Set to `http` to serve Streamable HTTP instead of stdio. | stdio |
| ALLOWED_HOSTS | No | Comma-separated hostnames the `Host` header may carry (e.g. `mcp.example.com`). Required when exposing the server remotely. | |
| MAX_XML_CHARS | No | Largest CFDI accepted, in characters. | 2000000 |
| MCP_AUTH_TOKEN | No | If set, every request needs `Authorization: Bearer <token>`. Recommended when exposing a self-hosted instance. | |
| ALLOWED_ORIGINS | No | Comma-separated origins allowed to call the server from a browser. | |
| MAX_CONCURRENT_CFDI | No | Number of `parse_cfdi` / `cfdi_status` calls served at once; the rest are told to retry. | 4 |
Instructions
Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.
This server publishes no instructions, or was last inspected before Glama recorded them.
Capabilities
Features and capabilities supported by this server
Protocol revision2025-11-25
| Capability | Details |
|---|---|
| tools | {
"listChanged": true
} |
Tools
Functions exposed to the LLM to take actions
| Name | Description |
|---|---|
| validate_rfcA | Validate a Mexican RFC (Registro Federal de Contribuyentes) — the SAT taxpayer ID — and break it into its parts. Works for both shapes: 13 characters for a persona física (individual) and 12 for a persona moral (company). The last character is a modulus-11 check digit over the preceding ones, and this checks it. Two SAT-issued generics are special-cased and reported as such:
The tool reports Structural validity is not registration. A well-formed RFC may belong to nobody. Only the SAT can say whether one is registered and active, and this server never asks. Args:
Returns: { input, normalized, valid, kind, is_generic, generic_note, parts{iniciales, fecha, homoclave, digito}, birth_date, expected_check_digit, check_digit_satisfied, errors[{code, message}], findings[] }. Example: "Is GODE561231GR8 a valid RFC?" -> validate_rfc(value="GODE561231GR8"). |
| validate_curpA | Validate a Mexican CURP (Clave Única de Registro de Población) — the 18-character population ID issued by RENAPO — and decode everything it encodes: birth date, sex, state of birth and the century marker. The 18th character is a base-37 modulus-10 check digit over the first 17, and this checks it. Two details it gets right that a regex does not:
Names that would spell one of RENAPO's inconvenient words are flagged: a real CURP carries an X in the second position instead. Structural validity is not registration. Only RENAPO can confirm a CURP belongs to a real person, and this server never asks it. Args:
Returns: { input, normalized, valid, parts{...}, birth_date, sex, sex_label, state_key, state_name, century_marker, expected_check_digit, errors[{code, message}], findings[] }. Example: "Decode BOXW310820HNERXN09" -> validate_curp(value="BOXW310820HNERXN09"). |
| validate_clabeA | Validate an 18-digit CLABE (Clave Bancaria Estandarizada, Banxico Circular 3/2012) — the account number every SPEI transfer in Mexico is addressed to — and name the bank behind it. Structure: 3 digits of bank + 3 of plaza (city/branch) + 11 of account + 1 control digit. The control digit is the part everybody gets wrong. The weights cycle 3-7-1, and each weighted product is reduced modulo 10 before being added to the sum. Implementations that sum the products first — the Luhn habit — accept and reject the wrong numbers. This uses the correct algorithm. The bank code is resolved against a curated subset of Banxico's participant catalogue. A code that is not in the subset is reported as unknown rather than given an invented name. The plaza code is reported verbatim: the full plaza catalogue is not bundled, so no city is guessed. Args:
Returns: { input, normalized, formatted, valid, parts{banco, plaza, cuenta, digito}, bank_code, bank_name, plaza_code, plaza_note, expected_check_digit, errors[{code, message}], findings[] }. Example: "Which bank is CLABE 012180012345678903?" -> validate_clabe(value="012180012345678903"). |
| validate_nssA | Validate an 11-digit NSS (Número de Seguridad Social) issued by the IMSS, and split it into its fields: 2 digits of subdelegación, 2 of the year the holder was registered, 2 of the year of birth, 4 of serial, and a Luhn check digit over the first ten. The two year fields are informational only. The IMSS has issued numbers whose years do not line up with the holder's records, so a mismatch is not grounds to reject a number — only the check digit is. Args:
Returns: { input, normalized, valid, parts{subdelegacion, anioAlta, anioNacimiento, folio, digito}, expected_check_digit, errors[{code, message}], findings[] }. Example: "Is 12345678903 a valid NSS?" -> validate_nss(value="12345678903"). |
| generate_test_dataA | Generate structurally valid Mexican identifiers for fixtures, database seeds and demos — generated, not taken from any real record. For a person each record carries a coherent set: the RFC and the CURP are derived from the same name, sex, birth date and state, the CLABE's bank code is a real Banxico participant, and the NSS satisfies its Luhn digit. For a company, a razón social with a matching persona-moral RFC and a CLABE. Why generated instead of hand-written: an RFC or CURP typed by hand almost never satisfies its check digit, so it fails the first validation your own code runs, and a seed file full of 'AAAA010101AAA' teaches your tests nothing. These pass validation but are nobody's on purpose. They are generated from common names, so a CURP or phone number can coincide with a real person's by chance; they are not looked up at the SAT, RENAPO, IMSS or Banxico — do not send them to the SAT's status service or to a PAC. Args:
Returns: { kind, count, people[{nombre, apellido_paterno, apellido_materno, sexo, fecha_nacimiento, entidad, entidad_nombre, rfc, curp, clabe, banco, nss, codigo_postal, telefono, email}], companies[{razon_social, rfc, clabe, banco, codigo_postal, fecha_constitucion}], findings[] }. Example: "Give me 5 fake Mexican customers with valid RFC and CURP" -> generate_test_data(kind="person", count=5). |
| parse_cfdiA | Turn the XML of a Mexican electronic invoice (CFDI 4.0) into structured JSON: header, issuer, receiver, every line item with its transferred and withheld taxes, the tax totals, and the Timbre Fiscal Digital (UUID, stamp date, SAT certificate number, PAC's RFC) — or Three things this does beyond reading attributes:
It walks the document by local element name, so it does not care whether the PAC used the What this does NOT do: it does not verify the digital signature, and it does not ask the SAT anything. A document that parses cleanly can still be cancelled, or have been fabricated wholesale. Use Args:
Returns: { version, serie, folio, fecha, tipo(+label), forma_pago(+label), metodo_pago(+label), moneda, tipo_cambio, sub_total, descuento, total, lugar_expedicion, exportacion, condiciones_de_pago, no_certificado, emisor{rfc, nombre, regimen(+label), rfc_valid, rfc_kind, rfc_errors}, receptor{… domicilio, uso(+label)}, conceptos[{descripcion, clave_prod_serv, cantidad, clave_unidad, unidad, valor_unitario, importe, descuento, objeto_imp(+label), traslados[], retenciones[]}], concepto_count, total_trasladados, total_retenidos, stamped, timbre{uuid, fecha_timbrado, no_certificado_sat, rfc_prov_certif} | null, arithmetic{…}, findings[] }. Example: "Read this invoice and tell me who issued it and for how much" -> parse_cfdi(xml="<cfdi:Comprobante …>"). |
| cfdi_statusA | Ask the SAT whether an invoice actually exists and is still live. This queries the public Pass either the four values the SAT keys on (issuer RFC, receiver RFC, total, UUID) or the whole What comes back, each with its meaning spelled out:
Fail-soft by design. The SAT publishes no rate limit, no SLA and no status page, and the endpoint does go down. On timeout, refusal or a malformed answer this returns Args (either shape):
Returns: { available, unavailable_reason, endpoint, expression, attempts, elapsed_ms, source, status{codigo_estatus, query_outcome, estado, document_state, document_meaning, es_cancelable, cancellable_state, cancellable_meaning, estatus_cancelacion, cancellation_state, cancellation_meaning, validacion_efos, efos_state, efos_meaning, raw} | null, findings[] }. Example: "Is this invoice still valid?" -> cfdi_status(xml="<cfdi:Comprobante …>"). |
| sat_catalog_lookupA | Look up the code tables a CFDI is written in, without downloading the SAT's spreadsheet. Nine catalogues, all bundled — this tool never touches the network.
With no Two honesty notes carried in the output: the bank list is a curated subset of Banxico's participant catalogue rather than the whole thing, and the CURP state keys are RENAPO's own — they do not match INEGI or ISO 3166-2:MX codes. Args:
Returns: { catalog, official_name, authority, description, used_in, query, match_type, total_entries, match_count, entries[{code, label}], truncated, notes[] }. Example: "What does UsoCFDI G03 mean?" -> sat_catalog_lookup(catalog="uso_cfdi", query="G03"). |
Prompts
Interactive templates invoked by user choice
| Name | Description |
|---|---|
No prompts | |
Resources
Contextual data attached and managed by the client
| Name | Description |
|---|---|
No resources | |