Skip to main content
Glama
README.md
# erp2pdf-studio-mcp

[![CI](https://github.com/c4pointer/erp2pdf-studio-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/c4pointer/erp2pdf-studio-mcp/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)

An [MCP](https://modelcontextprotocol.io) server (stdio) that lets an AI agent
build and test PDF templates on [erp2pdf.com](https://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](https://erp2pdf.com/api-docs#template-api);
a compact, agent-friendly summary is at
[erp2pdf.com/llms.txt](https://erp2pdf.com/llms.txt).

## Contents

- [Requirements](#requirements)
- [Get an API token](#get-an-api-token)
- [Install](#install)
- [Configure your MCP client](#configure-your-mcp-client)
- [Environment variables](#environment-variables)
- [Tools](#tools)
- [Example session](#example-session)
- [Development](#development)
- [Versioning and support](#versioning-and-support)
- [License](#license)

## Requirements

- Python 3.10 or newer (or [`uv`](https://docs.astral.sh/uv/), which can
  fetch a suitable Python for you).
- An [erp2pdf.com](https://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](https://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`](https://docs.astral.sh/uv/guides/tools/):

```bash
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`):

```bash
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`](https://pypi.org/project/mcp/) 2.3.0 (the official MCP
Python SDK) and [`httpx`](https://www.python-httpx.org/).

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

```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

```bash
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):

```json
{
  "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

```bash
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](CONTRIBUTING.md) before opening a pull request.

## Versioning and support

The project follows [Semantic Versioning](https://semver.org). While it is
0.x, a minor version may change tool parameters; every change is listed in
[CHANGELOG.md](CHANGELOG.md). Releases are tagged `vX.Y.Z`.

- Bugs and feature requests:
  [GitHub issues](https://github.com/c4pointer/erp2pdf-studio-mcp/issues).
- Account, plan or token questions: hello@erp2pdf.com.
- Security issues: see [SECURITY.md](SECURITY.md). Never post a token in an
  issue.

## License

[MIT](LICENSE). Copyright (c) 2026 Oleg Zubak (erp2pdf.com).

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