Skip to main content
Glama

erp2pdf-studio-mcp

CI License: MIT Python 3.10+

An MCP server (stdio) that lets an AI agent build and test PDF templates on erp2pdf.com without the Studio UI. The agent can discover the Odoo models and fields, create a template from a starter or from scratch, validate it, see what every expression prints for a real record, render a watermarked preview (PDF, PNG or HTML), write the HTML email templates Odoo sends, and translate both kinds of template into any language.

Each tool maps 1:1 onto an endpoint of the erp2pdf.com Template API (/api/v1/studio/*). The layout format, the expression language, error codes and rate limits are documented at erp2pdf.com/api-docs#template-api; a compact, agent-friendly summary is at erp2pdf.com/llms.txt.

Contents

Related MCP server: ERPipe

Requirements

  • Python 3.10 or newer (or uv, which can fetch a suitable Python for you).

  • An erp2pdf.com account on the Pro plan or higher. Other plans get 402 upgrade_required from the Template API.

  • A dedicated API token issued with Allow Template API ticked (see below). Never reuse the Odoo connector's token.

  • To work with live Odoo data (search_records, real sample records), an Odoo database connected to your erp2pdf.com account. Without it the API falls back to bundled sample records.

Get an API token

  1. Sign in at erp2pdf.com and open the cabinet: API tokens (https://erp2pdf.com/cabinet-tokens) -> New agent token.

  2. Name it after the agent (for example "Claude") and tick Allow Template API (AI agents, MCP). Only tokens issued with that box ticked can call the Template API.

  3. Copy the token: it is shown once. Put it in your MCP client configuration (below), not in a repository.

Never reuse the Odoo connector's token. It is stored in Odoo's settings, has no Template API scope, and every tool call would fail with 403 token_scope. A dedicated token can also be revoked at any time without breaking your Odoo connection.

The server sends the token only as an Authorization: Bearer <token> header, and only to ERP2PDF_BASE_URL.

Install

Run it without a permanent install, with uvx:

uvx --from git+https://github.com/c4pointer/erp2pdf-studio-mcp erp2pdf-studio-mcp

Or install it with pip (in a virtual environment, or with pipx):

pip install git+https://github.com/c4pointer/erp2pdf-studio-mcp
erp2pdf-studio-mcp          # or: python -m erp2pdf_studio

To pin a release, append its tag to the URL, for example git+https://github.com/c4pointer/erp2pdf-studio-mcp@v0.4.1.

Dependencies: mcp 2.3.0 (the official MCP Python SDK) and httpx.

The server speaks MCP over stdio and is started by your MCP client; you do not run it by hand. Started without ERP2PDF_API_KEY, it prints erp2pdf-studio-mcp: ERP2PDF_API_KEY is not set to stderr and exits with status 2.

Configure your MCP client

Replace <token> with the token from the cabinet.

Claude Desktop

Settings -> Developer -> Edit config opens claude_desktop_config.json:

{
  "mcpServers": {
    "erp2pdf-studio": {
      "command": "uvx",
      "args": [
        "--from",
        "git+https://github.com/c4pointer/erp2pdf-studio-mcp",
        "erp2pdf-studio-mcp"
      ],
      "env": {
        "ERP2PDF_API_KEY": "<token>",
        "ERP2PDF_OUTPUT_DIR": "/Users/you/Desktop/erp2pdf"
      }
    }
  }
}

Restart Claude Desktop after saving. If uvx is not on the PATH Claude Desktop sees, use its absolute path (which uvx), or install the package with pip and set "command" to the absolute path of erp2pdf-studio-mcp.

Claude Code

claude mcp add erp2pdf-studio \
  -e ERP2PDF_API_KEY=<token> \
  -- uvx --from git+https://github.com/c4pointer/erp2pdf-studio-mcp erp2pdf-studio-mcp

Add --scope user to make it available in every project. For a project-level .mcp.json, use the same mcpServers block as for Claude Desktop and keep the token out of version control: write "ERP2PDF_API_KEY": "${ERP2PDF_API_KEY}" and Claude Code takes the value from your environment.

Cursor

.cursor/mcp.json in the project (or ~/.cursor/mcp.json for all projects):

{
  "mcpServers": {
    "erp2pdf-studio": {
      "command": "uvx",
      "args": [
        "--from",
        "git+https://github.com/c4pointer/erp2pdf-studio-mcp",
        "erp2pdf-studio-mcp"
      ],
      "env": {
        "ERP2PDF_API_KEY": "<token>"
      }
    }
  }
}

Any other MCP client that can start a stdio server works the same way: command uvx, the arguments above, and the environment variables below.

Environment variables

Variable

Required

Default

Description

ERP2PDF_API_KEY

yes

-

API token from the erp2pdf.com cabinet, issued with "Allow Template API".

ERP2PDF_BASE_URL

no

https://erp2pdf.com

Base URL of the erp2pdf.com deployment to talk to.

ERP2PDF_OUTPUT_DIR

no

~/.cache/erp2pdf-studio-mcp (%LOCALAPPDATA%\erp2pdf-studio-mcp on Windows)

Where render_preview saves PDF and PNG previews. Created if missing, readable only by you.

Tools

Every tool returns JSON text. A failed call is an MCP tool error whose text is JSON {status, code, message, errors?, warnings?}; for an invalid layout, errors lists {path, code, message} so the agent can fix the exact slot. Previews carry a PREVIEW watermark and cost no credits.

Odoo data

Tool

What it does

list_models

List the Odoo models a template can bind to (live from your Odoo when connected, else bundled samples).

get_fields

Fields of a model, plus dotted paths one relation deep (partner_id.name, order_line.price_subtotal).

search_records

Search real records in your Odoo to pick a record_id for values and previews (needs a connected Odoo).

get_sample_record

The record previews are rendered with, to show the data shape.

PDF templates

Tool

What it does

list_templates

List the account's templates (id, name, bound model, slot count).

get_template

Get one template with its full layout and mapping.

create_template

Create a template from a Studio layout; it is validated first and nothing is saved on errors.

update_template

Replace the layout, or edit in place: upsert or delete slots, set top-level keys.

delete_template

Delete a template.

duplicate_template

Copy a template.

validate_layout

Validate a layout without saving, optionally evaluating every expression against a record.

get_values

What every {{ }} expression prints for one record, per slot and per table row.

render_preview

Render a watermarked preview as PDF or PNG (saved to ERP2PDF_OUTPUT_DIR) or HTML (returned inline). A PNG also comes back as an image the agent can look at.

list_starters

List the starter layouts (blank, quotation, invoice, purchase, product) and the styles each comes in.

create_from_starter

Create a template from a starter in a style (classic, folder, wave, diagonal, sidebar, bigtotal, light, boxed, striped, bubble, elegant) and a palette/font preset.

preview_starter

Look at a starter style in a palette and font pairing before creating anything (an image of page 1 on sample data).

Design

Tool

What it does

get_capabilities

Everything a layout can hold: kinds and keys with allowed values (tables of order lines, typed grid tables, chart blocks, sections of grouped blocks), gradients, free vector path slots, page background, theme, bundled fonts, limits, ready path recipes (folder tab, wave, chevron, hexagon, ...).

import_svg

Turn an SVG (markup or a local file) into a path slot: paths with fills, gradients and strokes; text and images are listed as dropped.

Header and footer parts

Tool

What it does

list_parts

List the shared header and footer parts.

create_part

Create a header or footer part.

update_part

Update a part; every template that uses it changes with it.

delete_part

Delete a part (refused while templates use it, unless force=true).

Translations (PDF and email templates)

Tool

What it does

get_translations

Every translatable string with its translation, status and glossary suggestions per language.

set_translations

Translate many languages in one call: merge or replace, add or remove languages, fill from the glossary.

Email templates

Tool

What it does

list_email_templates

List the HTML email templates sent from Odoo by the erp2pdf.com connector.

get_email_template

Get one email template: subject, body, translations and the fields it reads.

create_email_template

Create an email template, optionally attaching a PDF template.

update_email_template

Change an email template; only the fields given change.

delete_email_template

Delete an email template.

preview_email

Render a saved or draft email for one record, in a given language and timezone.

get_values, render_preview and preview_email take language (for example de_DE), which picks translations and the date presets, and timezone (an IANA name such as Europe/Berlin), where datetimes print.

The full parameter list of each tool is in its MCP schema, which your client shows to the agent.

Example session

Make me a quotation template for sale.order with our customer reference under the address, check it and show me page 1.

What the agent does:

  1. get_fields("sale.order") finds client_order_ref.

  2. create_from_starter("quotation", "Quotation (AI)") creates the template with a line table, totals and the shared header and footer.

  3. update_template(id, upsert_slots=[...]) adds a text slot under the partner address.

  4. validate_layout(...) and get_values(template_id=id) confirm that every expression prints what it should.

  5. render_preview(template_id=id, output="png") saves page 1, returns its path and shows the image to the agent.

Make our invoice bold: a dark wave across the top in our brand blue, our logo from logo.svg, the total set large.

  1. get_capabilities() shows the keys, gradients, fonts and path recipes.

  2. create_from_starter("invoice", "Invoice (wave)", style="wave").

  3. update_template(id, layout_set={"theme": {"primary": "#0b3d91", ...}}) recolours every block that refers to the theme.

  4. import_svg(path="logo.svg", w=120, h=40), then update_template(id, upsert_slots=[{"id": "logo", "kind": "path", ...}]).

  5. render_preview(template_id=id, output="png"): the agent looks at the page and adjusts until it reads well.

Translate it into German and French, and the quotation email too.

  1. get_translations(id) lists the strings, with glossary suggestions.

  2. set_translations(id, fill_from_glossary=["de", "fr"]), then set_translations(id, translations={"de": {...}, "fr": {...}}) for the rest. A string with values (Quotation {{ name }}) keeps its {{ }}.

  3. render_preview(template_id=id, output="png", language="de_DE").

  4. get_translations(email_id, kind="email"), then set_translations(email_id, kind="email", ...), then preview_email(email_id, language="fr_FR").

Development

git clone https://github.com/c4pointer/erp2pdf-studio-mcp
cd erp2pdf-studio-mcp
python -m venv .venv
. .venv/bin/activate
pip install -e ".[test,dev]"
ruff check .
pytest -q

The tests mock the HTTP layer with httpx.MockTransport and drive the server through the SDK's list_tools / call_tool; they need no token and no network. To try a local change in an MCP client, point the client's "command" at .venv/bin/erp2pdf-studio-mcp.

Code layout:

  • erp2pdf_studio/client.py - HTTP client, one method per Template API endpoint; validates model names and ids before they become URL path segments.

  • erp2pdf_studio/tools.py - the MCP tools (plain async methods, testable without the SDK).

  • erp2pdf_studio/server.py - registers the tools on the MCP server and runs it over stdio.

See CONTRIBUTING.md before opening a pull request.

Versioning and support

The project follows Semantic Versioning. While it is 0.x, a minor version may change tool parameters; every change is listed in CHANGELOG.md. Releases are tagged vX.Y.Z.

License

MIT. Copyright (c) 2026 Oleg Zubak (erp2pdf.com).

Available Tools

30 tools
create_email_templateB

Create an email template. Subject and body use the PDF templates' expressions: {{ expr }} prints a value HTML-escaped, {{{ expr }}} inserts it as cleaned HTML. Use inline styles and tables (email clients). {{ odoo_url }} is the connected Odoo's https address: {{ odoo_url }}/logo.png?company={{ company_id.id }} is the company logo. attach_template_id = a PDF template to attach when sent.

Inputs: name (required), subject, body_html, bind_model, +2 more.

create_from_starterA

Create a template from a starter layout (same as Studio's New template dialog). style picks the look (default classic): folder = light band with a folder tab, wave = dark wave header, diagonal = dark band with a slanted edge, sidebar = coloured column, bigtotal = the amount to pay set large. Every colour is a theme reference, so update_template(layout_set={"theme": {...}}) rebrands it. theme picks a palette and fonts at creation: a palette key ("ocean"), or {"palette": "ocean", "fonts": "editorial", } (list_starters lists presets.palettes and presets.fonts). Classic reuses the shared 'Company header'/'Company footer' parts (created on first use); the bold styles draw their own letterhead. model is only for the blank starter.

Inputs: name (required), starter (required), model, style, +4 more.

create_partC

Create a header or footer part: {slots[]} in page coordinates (a header near the top, a footer near the bottom of the A4 page).

Inputs: kind (required), name (required), layout (required), description.

create_templateB

Create a template from a Studio layout {bind_model, slots[], master_refs?, translations?}. The layout is validated first; on errors nothing is saved and the errors carry JSON paths.

Inputs: name (required), layout (required), mapping, description.

delete_email_templateD

Delete an email template.

Inputs: email_template_id (required).

delete_partA

Delete a part. Refused (part_in_use) while templates use it, unless force=true.

Inputs: part_id (required), force.

delete_templateC

Delete a template.

Inputs: template_id (required).

duplicate_templateC

Copy a template (default name: ' (copy)').

Inputs: template_id (required), name.

get_capabilitiesA

Read this before designing. Everything a layout can hold, from the same rules the validator applies: slot kinds and their keys with types and allowed values (text: letterSpacing, lineHeight, textTransform, valign, bgColor...; image: mask, grayscale; every slot: opacity, shadow, rotation, blendMode), the paint format (hex, theme., or a linear/radial gradient object), the path kind (free vector drawing: viewBox + paths[{d, fill, gradient, stroke, ...}]), page background, theme keys, bundled fonts, page sizes, limits, and ready path_recipes (folder tab, wave, diagonal band, chevron, hexagon, blob, corner arc) to copy into a path slot.

Inputs: no named parameters.

get_email_templateB

Get one email template: subject, body_html, translations and the record fields it reads.

Inputs: email_template_id (required).

get_fieldsC

Fields of an Odoo model, plus paths: dotted paths one relation deep (partner_id.name, order_line.price_subtotal). A path with in_table belongs inside a table (repeater) bound to that value, where you write it as row_path (e.g. {{ price_subtotal }}).

Inputs: model (required), search.

get_sample_recordD

The record previews are rendered with (real when a connector is configured, else the bundled sample): shows the data shape.

Inputs: model (required), record_id.

get_templateB

Get one template with its full layout and mapping.

Inputs: template_id (required).

get_translationsA

Every translatable string of a PDF template (kind=pdf, header and footer parts included) or an email template (kind=email: subject and body text), with its translation per target language, a status per language (translated, inherited, override, invalid, missing), glossary suggestions for missing cells and missing counts per language. A string that prints values keeps them as {{ placeholders }}; an email string keeps its inline tags (b, strong, i, em, u, a, span, code, br) as numbered placeholders <1>...</1> and <2/>, listed in placeholders (the element each stands for in tags).

Inputs: template_id (required), kind.

get_valuesA

What every {{ }} expression prints for one record, per slot (slots: path, template, value/error) and per table row (tables). Pass template_id for a saved template or layout for a draft. The cheapest way to check expressions without reading a PDF. language picks formatDate's "short"/"long"/"datetime" presets; timezone (IANA, Europe/Kyiv) is where datetimes print, unless the live record brings the document's own.

Inputs: model, layout, mapping, language, +3 more.

import_svgA

Turn an SVG (a logo, an illustration, a shape drawn elsewhere) into a path slot body: returns {viewBox, paths, warnings, box, keepAspect}. Pass the markup as svg, or path to a local .svg file (max 1 MB). Create the slot as {"kind": "path", "x", "y", "w": box.w, "h": box.h, "viewBox", "keepAspect", "paths"}, or pass w/h for the size you will place it at (stroke widths are in pt for that size). Text, images, filters and masks are dropped and listed in warnings.

Inputs: h, w, svg, path.

list_email_templatesA

List the account's HTML email templates (sent from Odoo by the connector's 'Send by email' wizard).

Inputs: no named parameters.

list_modelsA

List the Odoo models a template can bind to (live from the customer's Odoo when a connector is configured, else bundled samples). Use the model value as layout.bind_model.

Inputs: search.

list_partsA

List the shared header/footer parts (templates use them through layout.master_refs = {header: id, footer: id}).

Inputs: kind.

list_startersB

Starting points: blank, quotation, invoice, purchase, product, each with the styles it comes in (classic, folder, wave, diagonal, sidebar, bigtotal) and whether a style uses the shared header/footer.

Inputs: no named parameters.

list_templatesA

List the account's templates (id, name, bind_model, slot_count).

Inputs: no named parameters.

preview_emailA

Render an email for one record: a saved one (email_template_id) or a draft (subject + body_html, optional translations). language (de_DE, fr...) shows it as a customer with that language gets it; timezone (IANA) is where its datetimes print. Returns {subject, html, errors, source, record}; no credits.

Inputs: subject, language, timezone, body_html, +4 more.

preview_starterA

Look at a starter style before creating anything: page 1 on a fixed sample document, re-skinned by a palette and a font pairing (preset keys from list_starters), as an image. Width 240-960 px. Use it to compare styles and palettes with the user, then create_from_starter(starter, name, style=..., theme=palette).

Inputs: starter (required), font, style, width, +1 more.

render_previewB

Render a preview (PREVIEW watermark, no credits) of a saved template (template_id) or an unsaved layout. output=html returns the HTML inline; pdf/png are saved to a local file and the path is returned (png = page 1). With output=png and show=true (default) the image itself comes back too: look at it to judge the design (alignment, overlaps, clipped text, contrast) and iterate.

Inputs: show, model, layout, output, +5 more.

search_recordsA

Search real records in the customer's Odoo (needs a configured connector) to pick a record_id for get_values / render_preview.

Inputs: model (required), limit, search.

set_translationsA

Translate a template, many languages in one call. translations = {lang: {source string: text}} (lang: de, de_DE, fr_CH...; source strings exactly as get_translations lists them; null deletes a cell). A translation must keep exactly the source's {{ expressions }} and, in an email, its numbered tags <1>...</1>, <2/> (any order, each tag closed after it opens), else nothing is saved and the errors say which. replace=true replaces each language sent as a whole (PUT); default merges (PATCH). fill_from_glossary fills empty cells of the given languages from the built-in glossary. Returns the updated report plus changed and warnings.

Inputs: template_id (required), kind, replace, languages, +6 more.

update_email_templateC

Change an email template; only the fields given change. remove_attachment=true stops attaching a PDF.

Inputs: email_template_id (required), name, subject, body_html, +4 more.

update_partB

Update a part's name, description and/or layout. Every template that uses the part changes with it.

Inputs: part_id (required), name, layout, description.

update_templateA

Change a template. Either send a whole new layout, or edit in place: upsert_slots adds slots or replaces those with the same id, delete_slots removes top-level slots by id, layout_set sets top-level keys such as master_refs or translations (null removes).

Inputs: template_id (required), name, layout, mapping, +4 more.

validate_layoutA

Validate a layout without saving: returns {ok, errors[], warnings[], stats}. Each issue has a JSON path (slots[3].row_template[1].content), a code and a message. With evaluate=true every expression is also run against a sample record; runtime failures come back as warnings.

Inputs: layout (required), kind, model, mapping, +2 more.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    B3/5.0

    Scored across 30 tools

    Disambiguation4/5

    The surface splits cleanly into sub-domains (models/records, PDF templates, parts, starters, email templates, translations, preview/validation), and the descriptions explicitly cross-reference which tool to use when. A few boundaries blur: three creation paths (create_template, create_from_starter, duplicate_template) and three preview tools whose names invert verb placement (render_preview vs preview_starter/preview_email) could still cause misselection, as could the family of get_* tools (get_fields, get_capabilities, get_values, get_sample_record), which all mean different kinds of 'get'.

    Naming Consistency4/5

    Nearly everything is snake_case with a leading verb, and the list_*/get_*/create_*/update_*/delete_* families are uniform across templates, parts, and email templates. The deviation is verb ordering in the preview trio (render_preview vs preview_starter, preview_email) and in create_from_starter, where the verb is followed by a preposition rather than a noun.

    Tool Count3/5

    At 30 tools this sits above the comfortable band and is heavy for an agent to hold in working memory. The breadth is real (PDF design, shared parts, starters, SVG import, translations, email templates, preview, validation), and nearly every tool maps to a distinct operation, but some could consolidate — e.g. list_starters/preview_starter and create_template/create_from_starter.

    Completeness4/5

    CRUD is complete for PDF templates, shared parts, and email templates, and the design loop is closed by validate_layout, get_values, render_preview, and preview_email. Minor gaps remain: there is no get_part for fetching a single part, no duplicate for email templates, and no tool to inspect or edit existing translations per-cell beyond the bulk get/set pair, but these are workable.

    Maintenance

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables AI assistants to generate PDF documents from templates or raw HTML using natural language, with tools for template creation, PDF generation, and utility operations.
      24 npm
      1
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables AI agents to interact with Odoo ERP through natural language, providing tools and prompts for data operations and record management.
      MIT
    • F
      license
      Not graded
      quality
      C
      maintenance
      Enables AI assistants to interact with Odoo ERP via the external API. It supports listing models, describing schemas, searching, aggregating, creating, updating, deleting records, and invoking business methods, with a configurable permission matrix for safety.
      -