Skip to main content
Glama
cmendezs

mcp-einvoicing-be

transform_to_ubl

Convert JSON invoice data to UBL 2.1 XML for separate validation or direct submission to a platform.

Instructions

Convert a structured JSON invoice payload to UBL 2.1 XML.

Unlike generate_invoice_be, this tool does not run validation after transformation. Intended as a conversion step when the caller will validate separately or submit directly to a platform that performs its own validation.

Returns a dict with:

  • xml: the generated UBL 2.1 XML string

  • warnings: list of non-fatal issues detected during transformation

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
dataYesSource invoice data matching the BEInvoice schema

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault

No arguments

Implementation Reference

  • The main handler function for the transform_to_ubl tool. Takes structured invoice data, validates it via BEInvoice model, generates warnings for missing VAT/IBAN, and calls render_ubl_invoice to produce UBL 2.1 XML.
    async def transform_to_ubl(
        data: Annotated[
            dict[str, Any],
            "Source invoice data matching the BEInvoice schema",
        ],
    ) -> dict[str, object]:
        """Convert a structured JSON invoice payload to UBL 2.1 XML.
    
        Unlike ``generate_invoice_be``, this tool does not run validation after
        transformation. Intended as a conversion step when the caller will validate
        separately or submit directly to a platform that performs its own validation.
    
        Returns a dict with:
        - ``xml``: the generated UBL 2.1 XML string
        - ``warnings``: list of non-fatal issues detected during transformation
        """
        invoice = BEInvoice.model_validate(data)
        warnings: list[str] = []
    
        if not getattr(invoice.buyer, "tax_id", None):
            warnings.append(
                "Customer VAT number is absent — acceptable for B2C but required for most B2B profiles."
            )
    
        terms = invoice.payment
        if invoice.payment_means_code == "30":
            iban = getattr(terms, "iban", None) if terms else None
            if not iban:
                warnings.append("Payment means is credit transfer (code 30) but no IBAN was provided.")
            elif not validate_iban(iban):
                warnings.append(f"IBAN '{iban}' does not appear to be valid.")
    
        xml_string = render_ubl_invoice(
            invoice=invoice,
            customization_id=CUSTOMIZATION_IDS["peppol-bis-3"],
            profile_id=PROFILE_IDS["peppol-bis-3"],
            namespaces=UBL_NAMESPACES,
        )
    
        return {"xml": xml_string, "warnings": warnings}
  • Registration of the transform_to_ubl handler via mcp.tool()(transform_to_ubl) inside _register_be_tools, which is invoked by mcp.register_plugin.
    def _register_be_tools(mcp: Any) -> None:
        """Register all Belgian e-invoicing tools onto the shared FastMCP instance."""
        mcp.tool()(_validator.validate_invoice_be)
        mcp.tool()(_validator.validate_pint_be)
        mcp.tool()(_generator.generate_invoice_be)
        mcp.tool()(transform_to_ubl)
        mcp.tool()(lookup_vat_be)
        mcp.tool()(check_peppol_participant_be)
        mcp.tool()(get_invoice_types_be)
  • BEInvoice Pydantic model used as the input schema for transform_to_ubl. Extends InvoiceDocument with Belgian-specific fields (profile, document_type, seller, buyer, lines, payment, etc.).
    class BEInvoice(InvoiceDocument):  # type: ignore[misc]
        """Belgian e-invoice.
    
        Extends ``InvoiceDocument`` with Belgium-specific fields: PINT-BE profile
        selection, Belgian party types, and Belgian payment terms.
        """
    
        document_type: Literal["380", "381", "383"] = Field(
            default="380",
            description="UNTDID 1001 code: 380=Invoice, 381=Credit note, 383=Debit note",
        )
        profile: Literal["peppol-bis-3", "pint-be"] = Field(
            default="peppol-bis-3",
            description="Belgian Peppol profile to apply",
        )
        seller: Supplier
        buyer: Customer
        lines: list[BEInvoiceLine] = Field(..., min_length=1)
        payment: BEPaymentTerms | None = Field(default=None)
        order_reference: str | None = Field(
            default=None, description="Purchase order reference (BT-13)"
        )  # noqa: E501
        contract_reference: str | None = Field(default=None, description="Contract reference (BT-12)")
        payment_means_code: str = Field(
            default="30",
            description="UNTDID 4461 payment means code (30=credit transfer)",
        )
    
        @field_validator("currency", check_fields=False)
        @classmethod
        def uppercase_currency(cls, v: str) -> str:
            return v.upper()
    
    
    # Re-export the core validation result — no Belgium-specific fields needed.
    ValidationResult = DocumentValidationResult
  • render_ubl_invoice — the core UBL XML rendering function called by transform_to_ubl. Serializes a BEInvoice to UBL 2.1 XML string.
    def render_ubl_invoice(
        invoice: BEInvoice,
        customization_id: str,
        profile_id: str,
        namespaces: dict[str, str],
    ) -> str:
        """Serialize a ``BEInvoice`` to a UBL 2.1 Invoice XML string."""
        root = Element(_q(_UBL_INVOICE_NS, "Invoice"))
    
        _el(root, _q(_CBC, "CustomizationID"), customization_id)
        _el(root, _q(_CBC, "ProfileID"), profile_id)
        _el(root, _q(_CBC, "ID"), invoice.number)
        _el(root, _q(_CBC, "IssueDate"), invoice.date)
        _el(root, _q(_CBC, "InvoiceTypeCode"), invoice.document_type)
        _el(root, _q(_CBC, "DocumentCurrencyCode"), invoice.currency)
    
        _el_opt(root, _q(_CBC, "Note"), getattr(invoice, "note", None))
    
        if invoice.order_reference:
            order_ref = SubElement(root, _q(_CAC, "OrderReference"))
            _el(order_ref, _q(_CBC, "ID"), invoice.order_reference)
    
        if invoice.contract_reference:
            contract_ref = SubElement(root, _q(_CAC, "ContractDocumentReference"))
            _el(contract_ref, _q(_CBC, "ID"), invoice.contract_reference)
    
        _render_party(root, "AccountingSupplierParty", invoice.seller)
        _render_party(root, "AccountingCustomerParty", invoice.buyer)
    
        _render_payment_means(root, invoice)
    
        if invoice.payment and invoice.payment.due_date:
            pt = SubElement(root, _q(_CAC, "PaymentTerms"))
            _el(pt, _q(_CBC, "Note"), f"Due: {invoice.payment.due_date}")
    
        _render_tax_total(root, invoice)
        _render_legal_monetary_total(root, invoice)
    
        for idx, line in enumerate(invoice.lines, start=1):
            line_el = SubElement(root, _q(_CAC, "InvoiceLine"))
            _el(line_el, _q(_CBC, "ID"), str(idx))
            qty_el = _el(line_el, _q(_CBC, "InvoicedQuantity"), format_quantity(line.quantity or 0))
            qty_el.set("unitCode", line.unit_code)
            ext_el = _el(
                line_el,
                _q(_CBC, "LineExtensionAmount"),
                format_amount((line.quantity or 0) * line.unit_price),
            )
            ext_el.set("currencyID", invoice.currency)
            item_el = SubElement(line_el, _q(_CAC, "Item"))
            _el(item_el, _q(_CBC, "Description"), line.description)
            _el_opt(  # noqa: E501
                item_el, _q(_CBC, "SellersItemIdentification"), getattr(line, "buyer_item_id", None)
            )
            cls_tax = SubElement(item_el, _q(_CAC, "ClassifiedTaxCategory"))
            _el(cls_tax, _q(_CBC, "ID"), line.vat_category.value)
            _el(cls_tax, _q(_CBC, "Percent"), str(line.vat_rate))
            ts = SubElement(cls_tax, _q(_CAC, "TaxScheme"))
            _el(ts, _q(_CBC, "ID"), "VAT")
            price_el = SubElement(line_el, _q(_CAC, "Price"))
            price_amt = _el(price_el, _q(_CBC, "PriceAmount"), format_amount(line.unit_price))
            price_amt.set("currencyID", invoice.currency)
    
        return tostring(root, encoding="unicode", xml_declaration=False)
  • CUSTOMIZATION_IDS and PROFILE_IDS constants used by transform_to_ubl to set the UBL customization and profile identifiers.
    """Peppol BIS Billing 3.0 constants for Belgium."""
    
    # UBL customizationID values (BT-24)
    CUSTOMIZATION_IDS: dict[str, str] = {
        "peppol-bis-3": ("urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0"),
        "pint-be": (
            "urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0"
            "#conformant#urn:fdc:www.nbb.be:2020:pintbe"
        ),
    }
    
    # UBL profileID values (BT-23)
    PROFILE_IDS: dict[str, str] = {
        "peppol-bis-3": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0",
        "pint-be": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0",
    }
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Discloses that no validation is performed, output format (xml, warnings), and non-fatal issues. With no annotations, but sufficient for a conversion tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three sentences plus a bullet list. Front-loaded with core purpose, no wasted words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a single-parameter tool with output schema, description covers purpose, usage, behavior, return values, and references to sibling tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema covers 100% of parameter (data), description adds context about it being a structured JSON invoice and mentions output. Adds value beyond schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Clearly states it converts a structured JSON invoice to UBL 2.1 XML, with specific verb and resource. Distinguishes from sibling generate_invoice_be.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly states when to use (caller will validate separately) and contrasts with generate_invoice_be which does validation.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cmendezs/mcp-einvoicing-be'

If you have feedback or need assistance with the MCP directory API, please join our Discord server