erp2pdf-studio-mcp
This MCP server lets an AI agent build, validate, preview, translate, and manage erp2pdf PDF templates and HTML email templates through the erp2pdf Template API.
Discover Odoo models, fields, dotted relation paths, sample records, and live records.
Create, update, duplicate, list, get, and delete PDF templates from raw layouts or starter layouts and styles.
Validate layouts and inspect what every expression prints for a record, per slot and table row.
Render watermarked previews as PDF, PNG, or inline HTML, including image previews for design review.
Explore layout capabilities, import SVGs as vector path slots, and manage shared header/footer parts.
Manage translations for PDF and email templates, including glossary filling and multi-language updates.
Create, update, list, get, delete, and preview HTML email templates, optionally attaching PDF templates.
Control language and timezone for previews, expression values, and email rendering.
Connects to an Odoo database linked to the erp2pdf.com account to discover models and their fields (including dotted relation paths like partner_id.name), search real records to pick a record_id for previews, and fetch sample records to show data shapes when building and testing PDF and email templates.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@erp2pdf-studio-mcp build an invoice PDF template for account.move and preview it"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
erp2pdf-studio-mcp
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_requiredfrom 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
Sign in at erp2pdf.com and open the cabinet: API tokens (
https://erp2pdf.com/cabinet-tokens) -> New agent token.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.
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-mcpOr 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_studioTo 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-mcpAdd --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 |
| yes | - | API token from the erp2pdf.com cabinet, issued with "Allow Template API". |
| no |
| Base URL of the erp2pdf.com deployment to talk to. |
| no |
| Where |
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 the Odoo models a template can bind to (live from your Odoo when connected, else bundled samples). |
| Fields of a model, plus dotted paths one relation deep ( |
| Search real records in your Odoo to pick a |
| The record previews are rendered with, to show the data shape. |
PDF templates
Tool | What it does |
| List the account's templates (id, name, bound model, slot count). |
| Get one template with its full layout and mapping. |
| Create a template from a Studio layout; it is validated first and nothing is saved on errors. |
| Replace the layout, or edit in place: upsert or delete slots, set top-level keys. |
| Delete a template. |
| Copy a template. |
| Validate a layout without saving, optionally evaluating every expression against a record. |
| What every |
| Render a watermarked preview as PDF or PNG (saved to |
| List the starter layouts (blank, quotation, invoice, purchase, product) and the styles each comes in. |
| 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. |
| 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 |
| Everything a layout can hold: kinds and keys with allowed values (tables of order lines, typed |
| Turn an SVG (markup or a local file) into a |
Header and footer parts
Tool | What it does |
| List the shared header and footer parts. |
| Create a header or footer part. |
| Update a part; every template that uses it changes with it. |
| Delete a part (refused while templates use it, unless |
Translations (PDF and email templates)
Tool | What it does |
| Every translatable string with its translation, status and glossary suggestions per language. |
| Translate many languages in one call: merge or replace, add or remove languages, fill from the glossary. |
Email templates
Tool | What it does |
| List the HTML email templates sent from Odoo by the erp2pdf.com connector. |
| Get one email template: subject, body, translations and the fields it reads. |
| Create an email template, optionally attaching a PDF template. |
| Change an email template; only the fields given change. |
| Delete an email template. |
| 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:
get_fields("sale.order")findsclient_order_ref.create_from_starter("quotation", "Quotation (AI)")creates the template with a line table, totals and the shared header and footer.update_template(id, upsert_slots=[...])adds a text slot under the partner address.validate_layout(...)andget_values(template_id=id)confirm that every expression prints what it should.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.
get_capabilities()shows the keys, gradients, fonts and path recipes.create_from_starter("invoice", "Invoice (wave)", style="wave").update_template(id, layout_set={"theme": {"primary": "#0b3d91", ...}})recolours every block that refers to the theme.import_svg(path="logo.svg", w=120, h=40), thenupdate_template(id, upsert_slots=[{"id": "logo", "kind": "path", ...}]).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.
get_translations(id)lists the strings, with glossary suggestions.set_translations(id, fill_from_glossary=["de", "fr"]), thenset_translations(id, translations={"de": {...}, "fr": {...}})for the rest. A string with values (Quotation {{ name }}) keeps its{{ }}.render_preview(template_id=id, output="png", language="de_DE").get_translations(email_id, kind="email"), thenset_translations(email_id, kind="email", ...), thenpreview_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 -qThe 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.
Bugs and feature requests: GitHub issues.
Account, plan or token questions: hello@erp2pdf.com.
Security issues: see SECURITY.md. Never post a token in an issue.
License
MIT. Copyright (c) 2026 Oleg Zubak (erp2pdf.com).
Available Tools
30 toolscreate_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
Scored across 30 tools
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'.
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.
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.
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
Related MCP Connectors
Render PDFs from templates you design once: list templates, check the data shape, preview, render.
Render HTML, URLs, and templates to PDF. AI drafts templates and fixes them from logs.
Send transactional pdfs for AI agents via SMTP. Templates included.
Design, publish and render PDF templates with Typst. Full template lifecycle over MCP.
Related MCP Servers
AlicenseNot gradedqualityDmaintenanceEnables 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 npm1MIT
ERPipeofficial
AlicenseNot gradedqualityBmaintenanceEnables AI agents to interact with Odoo ERP through natural language, providing tools and prompts for data operations and record management.MIT- AlicenseNot gradedqualityCmaintenanceEnables AI assistants to generate PDFs from templates using dynamic data, manage async PDF jobs, list templates, and check account credits and transaction history through natural language.61 npmMIT
- FlicenseNot gradedqualityCmaintenanceEnables 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.-