Skip to main content
Glama
OrtaMarco

mx-fiscal-mcp-server

by OrtaMarco

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault
HOSTNoBind 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
PORTNoListening port for the HTTP transport.3000
TRANSPORTNoSet to `http` to serve Streamable HTTP instead of stdio.stdio
ALLOWED_HOSTSNoComma-separated hostnames the `Host` header may carry (e.g. `mcp.example.com`). Required when exposing the server remotely.
MAX_XML_CHARSNoLargest CFDI accepted, in characters.2000000
MCP_AUTH_TOKENNoIf set, every request needs `Authorization: Bearer <token>`. Recommended when exposing a self-hosted instance.
ALLOWED_ORIGINSNoComma-separated origins allowed to call the server from a browser.
MAX_CONCURRENT_CFDINoNumber 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

CapabilityDetails
tools
{
  "listChanged": true
}

Tools

Functions exposed to the LLM to take actions

NameDescription
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:

  • XAXX010101000 (público en general) does NOT satisfy the check-digit algorithm — the arithmetic asks for a '4' where the SAT wrote a '0'. It is valid by decree, not by maths, which is exactly why so many home-grown validators wrongly reject invoices to the general public.

  • XEXX010101000 (residentes en el extranjero) DOES satisfy it on its own.

The tool reports is_generic and check_digit_satisfied separately so you never have to conflate the two.

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:

  • value (string): the RFC. Spaces, dashes and lower case are normalised away.

  • response_format ('markdown' | 'json'): output format (default 'markdown').

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:

  • The state keys are RENAPO's own. 'DF' is Ciudad de México, 'MC' is Estado de México, 'NE' means born abroad. They do not match the INEGI or ISO 3166-2:MX codes, so a lookup against those tables silently mislabels people.

  • The homoclave character carries the century. A digit (position 17) means born before 2000; a letter means from 2000 onwards. Without it, positions 5-10 ('99' as a year) are ambiguous.

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:

  • value (string): the CURP. Spaces, dashes and lower case are normalised away.

  • response_format ('markdown' | 'json'): output format (default 'markdown').

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:

  • value (string): the CLABE. Spaces and dashes are normalised away.

  • response_format ('markdown' | 'json'): output format (default 'markdown').

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:

  • value (string): the NSS. Spaces and dashes are normalised away.

  • response_format ('markdown' | 'json'): output format (default 'markdown').

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:

  • kind ('person' | 'company'): what to generate (default 'person').

  • count (integer 1-100): how many records (default 1).

  • response_format ('markdown' | 'json'): output format (default 'markdown').

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 timbre: null when the document was never stamped.

Three things this does beyond reading attributes:

  1. It labels the catalogue codes. '601' becomes 'General de Ley Personas Morales', 'G03' becomes 'Gastos en general', 'PPD' becomes 'Pago en parcialidades o diferido'. An unknown code is reported as unknown rather than guessed at.

  2. It validates both RFCs (issuer and receiver) with the full modulus-11 check, and flags the SAT generics.

  3. It checks the arithmetic: subtotal − discount + transferred − withheld should equal the declared total. A mismatch is a warning, not a verdict — rounding at the line level is allowed within a centavo.

It walks the document by local element name, so it does not care whether the PAC used the cfdi: and tfd: prefixes, different ones, or none.

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 cfdi_status for the SAT's own answer.

Args:

  • xml (string): the CFDI XML.

  • response_format ('markdown' | 'json'): output format (default 'markdown').

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 ConsultaCFDIService SOAP endpoint — the same service the QR code printed on every Mexican invoice points at — so it needs no credentials, no CSD and no PAC contract.

Pass either the four values the SAT keys on (issuer RFC, receiver RFC, total, UUID) or the whole xml, in which case they are derived from it with the same reader parse_cfdi uses. Deriving them from the XML is the more reliable route: the total must be formatted exactly the way the printed-representation spec demands (six decimals, trailing zeros trimmed), and a hand-typed total is the single most common cause of a spurious 'No Encontrado'.

What comes back, each with its meaning spelled out:

  • Estado — Vigente / Cancelado / No Encontrado.

  • EsCancelable — whether the issuer can cancel it unilaterally, needs the receiver's approval, or cannot cancel it at all.

  • EstatusCancelacion — whether a cancellation is in progress, was accepted, was rejected, or lapsed.

  • ValidacionEFOS — whether the issuer appears on the SAT's definitive 69-B list of companies that invoice simulated operations.

  • CodigoEstatus — the service's own result code.

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 available: false with the reason instead of raising — a failed lookup is a statement about the SAT, never about the invoice. Never report a document as invalid on the strength of an unreachable service. One retry, 10-second timeout.

Args (either shape):

  • xml (string), OR rfc_emisor + rfc_receptor + total + uuid (all strings).

  • response_format ('markdown' | 'json'): output format (default 'markdown').

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.

  • regimen_fiscal (c_RegimenFiscal) — tax regime of issuer and receiver

  • uso_cfdi (c_UsoCFDI) — what the receiver does with the invoice

  • forma_pago (c_FormaPago) — cash, transfer, card, …

  • metodo_pago (c_MetodoPago) — PUE vs PPD

  • tipo_comprobante (c_TipoDeComprobante) — I/E/T/N/P

  • objeto_imp (c_ObjetoImp) — whether a line item is subject to tax (CFDI 4.0)

  • impuestos (c_Impuesto) — ISR / IVA / IEPS

  • bancos_clabe — Banxico participants by the first three CLABE digits

  • estados_curp — RENAPO's state keys for CURP positions 12-13

With no query you get the whole catalogue. With one, an exact code match wins (leading zeros are ignored, so '1' finds '01'); failing that it falls back to a case- and accent-insensitive substring search over both code and label, so 'confianza' finds 626 and 'oaxaca' finds OC.

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:

  • catalog (enum): one of regimen_fiscal, uso_cfdi, forma_pago, metodo_pago, tipo_comprobante, objeto_imp, impuestos, bancos_clabe, estados_curp.

  • query (string, optional): an exact code, or text to search for.

  • response_format ('markdown' | 'json'): output format (default 'markdown').

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

NameDescription

No prompts

Resources

Contextual data attached and managed by the client

NameDescription

No resources