Talk to an Odoo ERP through its `drivethru_mcp` MCP server — discover the available Odoo tools at runtime and call them to look up eBay products/inventory, p...
---
name: drivethru-odoo
description: Talk to an Odoo ERP through its `drivethru_mcp` MCP server — discover the available Odoo tools at runtime and call them to look up eBay products/inventory, push eBay orders and read tracking, run the Accounts Payable PO→vendor-bill flow, review documents in the Documents app against their purchase orders and fix incorrect PO line pricing (the "check the Purchasing folder against the POs" / vendor-invoice pricing-review workflow, filing each document into Matched or Questions), schedule MRP production batches, drive vendor replenishment purchasing (run the replenishment report → curate lines → add to a PO → hand style/color/size/qty to the vendor's purchasing skill → write pricing + confirmation back and confirm the PO), and retrieve internal SOPs / best practices / policies from the Knowledge base scoped to the asking person's permissions. Use whenever the user needs to read from or write to Odoo, especially when you are answering a person inside an Odoo Discuss conversation.
version: 0.6.0
emoji: 🏭
homepage: https://www.odoo.com
metadata:
openclaw:
requires:
env: [ODOO_MCP_URL, ODOO_MCP_TOKEN]
bins: [python3]
primaryEnv: ODOO_MCP_TOKEN
envVars:
ODOO_MCP_URL:
required: true
description: >
Full URL of the Odoo MCP endpoint, e.g.
`https://odoo.example.com/drivethru_mcp/v1` (note the path — this is
the MCP server exposed by the `drivethru_mcp` Odoo module, not the
Odoo base URL).
ODOO_MCP_TOKEN:
required: true
description: >
The `drivethru.mcp_key` value from the Odoo `drivethru_mcp` module,
sent as `Authorization: Bearer`. Treat as a secret; never paste into
chat.
install:
uv:
- mcp>=1.9.0
- pypdf>=4.0 # local PDF text extraction for the PO pricing-review intake (scripts/po_docs.py)
---
# Odoo Drive Thru MCP integration
This skill gives you the Odoo **`drivethru_mcp`** MCP server — the curated Odoo
tool surface (`documents_*`, `ap_*`, `po_*`, `mfg_*`, `production_*`,
`replenish_*`, `knowledge_*`, `ebay_*`, `docs_*`) exposed over Streamable-HTTP.
`ODOO_MCP_URL` and `ODOO_MCP_TOKEN` are already configured for this agent.
**Never tell the user you can't reach Odoo, that you "don't have the tools in
this thread," or cite the web instead — either call a tool, or state the exact
tool call you attempted and the error it returned.**
## How you call Odoo (runtime-aware — read this first)
There are two ways the `drivethru_mcp` tools reach you. Work out which you have,
in this order:
1. **Native / callable MCP tools — preferred, no shell needed.** If the Odoo
tools are attached to you as native tools, **call them directly** — e.g.
`documents_list_folders {"name": "Purchasing"}`. This is how a chat agent
(an Odoo Discuss bot, etc.) uses Odoo; it does **not** need a shell.
- They may be **deferred / lazy-loaded**: if your runtime hides tools until
they're loaded, they won't show up until you discover them. **Search your
available tools / tool registry for names like `documents_list_folders`,
`ap_search_purchase_orders`, `po_get_messages`, and load them before
calling.** "I don't see them yet" is not "I don't have them."
- The names are the `drivethru_mcp` tool names (they may appear under a
server prefix, e.g. `…__documents_list_folders`); arguments and return
shapes match the table below.
2. **Shell script fallback — only if you have NO native tools but DO have a
shell.** Use the bundled client, which opens its own MCP connection with the
same env vars:
```bash
python3 scripts/odoo_mcp.py tools # list tools + JSON input schemas
python3 scripts/odoo_mcp.py call <tool> '{"...":"..."}' # call one (args as 2nd arg or on stdin)
```
Each invocation prints one JSON object on stdout, or
`{"error": {"type": ..., "message": ...}}` with a non-zero exit on failure.
**One-call smoke test (either path).** *"How many documents are in the
Purchasing folder that need matching?"* is a single call —
`documents_list_folders {"name": "Purchasing"}`, then read `document_count` on
the returned folder (native), or `python3 scripts/odoo_mcp.py call
documents_list_folders '{"name": "Purchasing"}'` (shell). A count back means
Odoo is reachable — proceed. No count? Go to **Troubleshooting** below; do not
fall back to guessing or the web.
`scripts/po_docs.py` is a shell helper for the **PO pricing review**: it lists a
Documents-app folder and returns each file's **extracted text** (never base64 or
a page render) so a many-document run doesn't fill the context window, plus a
`move` action to file a document into `Matched`/`Questions`. It's a
context-economy optimization for shell runtimes — with only native tools, use
`documents_search` + `documents_get` one document at a time instead (don't pull
a whole folder's base64 into context). See the pricing-review section below.
## Requirements: env vars set **and** the MCP attached
Two separate things must both be true — **the env vars alone are not enough:**
- **`ODOO_MCP_URL` and `ODOO_MCP_TOKEN` are set** in the agent's environment
(`ODOO_MCP_TOKEN` is sent as `Authorization: Bearer`). If either is missing,
the script path exits with `{"error": {"type": "config_error", ...}}` (exit 2)
and a native call can't authenticate. Secrets come from the environment; never
ask the user to paste the key into chat.
- **The `drivethru_mcp` server is attached to *this* agent** — as a native MCP
connection (native-tools path) **or** via a shell + Python (script path).
Setting the env vars does **not** by itself attach the tools; that's an
operator step (see **Operator setup** below), and it's the usual reason an
agent "has the skill but no tools."
## The live tool list is the source of truth
Tool names and their exact input schemas come from the server, not from memory —
**discover the live tools first** (load/list your native Odoo tools, or run
`python3 scripts/odoo_mcp.py tools`) and read a tool's `inputSchema` before
calling it. The current Odoo exposes these groups (names may evolve — trust the
live list over this table):
| Domain | Representative tools |
| ----------- | ------------------------------------------------------------------------ |
| eBay | `ebay_list_products`, `ebay_inventory`, `ebay_create_order`, `ebay_order_tracking` |
| Accounts Pay| `ap_search_purchase_orders`, `ap_get_purchase_order`, `ap_update_po_lines`, `ap_create_vendor_bill`, `ap_get_vendor_bill`, `ap_search_vendors`; PO chatter: `po_post_message`, `po_get_messages` |
| Documents app | `documents_list_folders`, `documents_search`, `documents_get`, `documents_create`, `documents_update`, `documents_post_message` — folders/files, chatter, and activities (the door to the Purchasing folder for the PO pricing review) |
| Production | `production_overview`, `production_schedule_queue`, `production_list_batches`, `production_get_batch`, `production_batch_supply`, `production_schedule_batch`, `production_plan_batch`, `production_bulk_schedule`, `production_list_workcenters`, `production_get_workcenter`, `production_list_production_centers`, `production_list_decoration_methods` |
| Mfg data (read-only) | `mfg_list_models`, `mfg_fields`, `mfg_read` — read ALL fields on any whitelisted manufacturing model (batches, POs, receipts, decorations, tracking, …) |
| Replenishment / Purchasing | `replenish_run_report`, `replenish_get_orderpoint`, `replenish_to_po`, `replenish_update_po`, `replenish_confirm_po`, `po_post_message`, `po_get_messages` |
| Internal knowledge (SOPs, best practices) | `knowledge_search_articles`, `knowledge_get_article` — permission-scoped to the asking person |
| Operator docs | `docs_list`, `docs_get` — fetch the operator reference for deeper context |
For a full production-scheduling pass (rank the queue → check receipts → place
batches on machines, never starting before goods are received), use the
dedicated **`drivethru-production-scheduler`** skill, which drives these same
tools.
## Operator setup: attaching the `drivethru_mcp` MCP (native tools)
For the **native-tools** path, the `drivethru_mcp` server has to be attached to
the agent as an MCP connection — **installing this skill and setting the env
vars does not do that.** A skill only ships instructions, scripts, and declared
Python deps; the skill format has no field that attaches an MCP server, so
attaching is an **operator/platform step**. Add `drivethru_mcp` to the agent's
MCP servers using the same env vars, as a Streamable-HTTP server:
```jsonc
// e.g. openclaw.json → mcp.servers
"drivethru_mcp": {
"url": "<ODOO_MCP_URL>", // the .../drivethru_mcp/v1 endpoint
"transport": "streamable-http",
"headers": { "Authorization": "Bearer <ODOO_MCP_TOKEN>" }
}
```
Once it's attached, the tools become native and callable — this is exactly how a
shell-less Discuss chat agent gets them. Two things to know:
- Some hosts **regenerate the agent config on every boot** from environment /
provisioning, so hand-editing the file on the volume won't survive a restart —
add the server through the host's agent/MCP configuration, not by hand.
- If your platform can't attach an arbitrary MCP server, the agent **must** have
a shell + `python3` so it can use `scripts/odoo_mcp.py` instead. An agent with
**neither** native tools **nor** a shell cannot reach Odoo — that's a
provisioning gap to fix, not something the agent can work around.
## Troubleshooting: the agent says it can't reach Odoo
If you (the agent) are about to say *"I don't have the Odoo tools in this
thread"* or reach for the web — **stop** and run this checklist first:
1. **Do you actually have the tools?** List/search your available tools for
`documents_list_folders` (or `ap_search_purchase_orders`). If your runtime
defers tools, they may need loading first — load them, then call. "I don't
see them" usually means "not loaded yet," not "not available."
2. **Run the one-call smoke test.** `documents_list_folders {"name":
"Purchasing"}`. A folder with a `document_count` back ⇒ you're connected;
keep going. If it errors, capture the exact error.
3. **Is `drivethru_mcp` attached to *this* agent?** No native tool **and** no
shell ⇒ the MCP was never attached (env vars alone don't attach it). That's
an operator step — see **Operator setup** above; tell the user plainly
instead of guessing.
4. **Are the env vars set?** A `config_error` from the script path, or an auth
error from a native call, means `ODOO_MCP_URL` / `ODOO_MCP_TOKEN` are missing
or wrong — an operator must set them.
5. **Report precisely.** If you still can't reach Odoo, state *the exact tool
call you attempted and the exact error you got* (e.g. `documents_list_folders
{"name":"Purchasing"}` → `connection_error: …`). Never fabricate an answer or
cite the web in place of a tool call.
## Working inside an Odoo Discuss conversation
You are often answering a **person in an Odoo Discuss DM** (your messages are
posted straight back into their thread). The bridge prefixes each forwarded
message with a fenced **`[Conversation context]`** block that names the person
and gives their **Odoo `user_id`**. Lift that `user_id` out and pass it to the
`knowledge_*` tools so internal-knowledge answers are scoped to what this
person is allowed to see (details below). If the context says the sender has no
linked Odoo user, don't guess a `user_id` — the knowledge tools can't be used
for them. So:
- **Be concise and conversational.** Reply in plain prose, not raw JSON dumps —
summarize what the tools returned. Surface a tool error's human-readable
message rather than the raw envelope.
- **Ask for missing information** instead of guessing identifiers. If you need a
PO, batch, or order id, look it up with a search/list tool first.
- **Confirm before writes.** Creating orders/bills, updating prices, and
scheduling/planning batches change live Odoo data. State exactly what you're
about to do and get the user's go-ahead before calling a write tool.
- Use `docs_list` / `docs_get` when you need the operator-level rules behind a
workflow (e.g. how vendor-bill matching or batch scheduling is meant to work).
## Typical flows
- "What eBay inventory do we have for A-1, B-2?" → `ebay_inventory`
`{"skus": ["A-1", "B-2"]}`.
- "Find the PO for vendor X and bill it." → `ap_search_purchase_orders` →
`ap_get_purchase_order` → (confirm) → `ap_create_vendor_bill`.
- "Check the docs in the Purchasing folder against the POs and fix the pricing."
→ the document-driven PO pricing-review flow below.
- "Show the production plan and schedule batch 142 onto workcenter 3." →
`production_overview` → `production_get_batch` `{"batch_id": 142}` → (confirm)
→ `production_schedule_batch`
`{"batch_id": 142, "primary_workcenter_id": 3, "date_planned_start": "..."}`.
- "Purchase all Adidas items for Batch12345." → the replenishment → purchasing
flow below.
- "What's our SOP for reprinting a damaged order?" → the internal-knowledge
flow below (`knowledge_search_articles` → `knowledge_get_article`).
## Internal knowledge retrieval (SOPs, best practices, policies)
You double as an **internal operations knowledge agent**. When someone asks a
"how do we…", "what's our policy on…", or "what's the SOP for…" question,
answer it from the Odoo **Knowledge** base rather than from memory — and only
from articles **that person is allowed to see**.
Two tools, both **permission-scoped to the asking person's `user_id`** (which
you take from the `[Conversation context]` block, see above):
1. **Search.** `knowledge_search_articles`
`{"user_id": <ctx uid>, "query": "reprint damaged order"}` — free text
matches title + body; `root_only` / `parent_id` walk the article tree. Only
articles this user may read come back (each with a breadcrumb `path`,
`snippet`, and `url`).
2. **Read in full.** `knowledge_get_article`
`{"user_id": <ctx uid>, "article_id": <id>}` — returns the article's
plain-text `body` plus metadata. Answer from this, not from the snippet.
**Permission handling.** The tools query Odoo *as that user*, so access is
enforced by Odoo's own rules — you don't manage permissions yourself. If
`knowledge_get_article` returns `accessible: false`, the person simply doesn't
have access to that article: tell them that plainly (offer to point them to
whoever owns it), and **don't** try to work around it or read it as anyone
else. Never pass a `user_id` you weren't given in the conversation context.
When you answer, summarize the SOP/policy in plain prose and cite the article
title (and `url` when present) so the person can open the source. Read
`docs_get {"slug": "knowledge"}` for the operator-level rules behind this.
## Document-driven PO pricing review (Purchasing folder)
When asked to *"go through the Purchasing folder and check each document's
pricing against the PO, fix any wrong lines, mark the PO checked, and file the
document"* (vendor order confirmations / acknowledgements / invoices), own the
whole loop. This runs many times a day over many documents, so two rules are
non-negotiable:
1. **Don't bloat the context window.** `documents_get` returns each file as
base64 and a PDF reader adds a page image — huge next to the ~300–600 chars
of text that matter. Extract text **out of context, once**, with the intake
helper instead of looping `documents_get`/a PDF reader over the folder:
```bash
python3 scripts/po_docs.py extract '{"folder": "Purchasing"}'
```
It fetches every file over the same MCP endpoint, decodes and extracts the
text locally, and returns compact JSON (`documents[].text`, no base64, no
render). Read that. Only fall back to `documents_get` for a document the
helper marks `needs_vision: true` (scanned/image-only), one at a time.
**No shell (native tools only)?** Enumerate with `documents_list_folders` /
`documents_search`, then read documents with `documents_get` **one at a
time** — read the text and drop the base64, never pulling a whole folder's
bytes into context.
2. **Every document ends in Matched or Questions.** The Purchasing folder has
sibling subfolders `Matched` and `Questions`; nothing stays in the inbox.
Per document: read the **PO# from the text** (not the filename — filenames may
carry the vendor's order number), then `ap_search_purchase_orders` →
`ap_get_purchase_order`, pair each line by **(style, color, size)**, compare
`price_unit`, and batch every fix into one `ap_update_po_lines`. Totals are a
cross-check only — a shipment acknowledgement is often one box of a
multi-shipment order, so a total gap explained by un-shipped lines is not a
discrepancy. Post a "checked" note with `po_post_message`. Then file it:
```bash
# reconciled (matched, or corrected with confidence)
python3 scripts/po_docs.py move '{"document_id": <id>, "to": "Matched", "under": "Purchasing"}'
# a real question — raise it on the document, assign Zach, then file to Questions
documents_post_message {"document_id": <id>, "body": "<question>", "activity_user": "Zach Tucker"}
python3 scripts/po_docs.py move '{"document_id": <id>, "to": "Questions", "under": "Purchasing"}'
```
Only escalate a **genuine** ambiguity (unreadable PO#, prices that don't
reconcile, unexpected/missing lines, wrong vendor). An unambiguous fix backed
by the vendor document is a Matched correction, not a question.
Full procedure, matching rules (partial shipments, freight/fees, PO#-from-body),
a worked example, and a **low-cost model recommendation** for running this at
volume are in
[`references/po_pricing_review.md`](references/po_pricing_review.md).
## Replenishment → vendor purchasing
When the user asks you to **buy** what the replenishment report says to buy for
a vendor (e.g. *"Purchase all items from Adidas for Batch12345"*), you own the
whole loop: read the report, curate the lines, put them on a PO, drive the
vendor's checkout, then write the result back and confirm. The
`drivethru_mcp` tools handle the Odoo side; a separate **vendor purchasing
skill** handles the actual buying.
Run it in this order — **confirm the curated set with the user before you
replenish, and again before you confirm the PO** (both are live writes):
1. **Run the report.** `replenish_run_report` with the vendor filter (e.g.
`{"vendor": "Adidas"}`). It rebuilds the replenishment report and returns
the shortage lines with `style` / `color` / `size` / `qty_to_order` / `price`
/ `production_batch_name` / `sale_order_name` / `is_out_of_stock`.
2. **Curate.** Keep only the lines the request calls for — *"for Batch12345"*
means keep lines whose `production_batch_name` matches; *"all Adidas"* means
keep them all. This filtering is your responsibility. Show the user the kept
lines (style/color/size/qty) and get a go-ahead.
3. **Add to a PO.** `replenish_to_po` `{"orderpoint_ids": [...]}`. This runs
`action_replenish` and **returns the draft PO(s)** with each line's
`line_id` + style/color/size/`product_qty`. Anything that couldn't be
bought (manufacture route, held sales order) comes back under `warnings` —
surface it.
4. **Find the vendor skill & purchase.** Locate the vendor's purchasing skill —
named `drivethru-<vendor>-click` (e.g. **`drivethru-adidas-click`**) — and
read its `SKILL.md`. For adidas that's a CLI: `echo '{...}' | python3
scripts/adidas.py create-purchase-order`, taking `{purchase_order:
{po_number, lines:[{style, size, quantity}]}, confirm}` (map the Odoo PO
`name` → `po_number`; color is optional — the adidas article encodes it).
**Dry-run first** (`confirm: false`), show the user, then `confirm: true`
places a real order. If no such skill exists, stop and tell the user; do not
invent a checkout.
5. **Write back.** Take the skill's result — match each result line to its Odoo
PO line by **(style, size)** to get the `line_id`, parse the `$`-prices to
numbers — and apply it with `replenish_update_po`: `price_unit` per
`line_id`, `vendor_order_number` ← the `confirmation_number`.
6. **Handle out-of-stock / exceptions.** The adidas skill pauses on a line it
can't fill and returns `status: "needs_confirmation"` with `out_of_stock`.
On that, `po_post_message` onto the PO with `issue_type: "out_of_stock"` and
an `activity_user_id` so purchasing/sales sees it — then **pause that PO**.
`po_get_messages` later to read the reply, then re-run the vendor skill with
the chosen policy (`on_insufficient_stock: "order"` / `"skip"`, or edited
lines to substitute). Do not confirm a PO with an unresolved issue.
7. **Confirm.** Once pricing is written and there are no open issues,
`replenish_confirm_po`. It confirms **without re-submitting to the vendor**
(the buy already happened via the skill).
The full hand-off contract (the exact dataset shape you pass the vendor skill
and the result shape you expect back) is in
[`references/replenishment_purchasing.md`](references/replenishment_purchasing.md).
Read `docs_get {"slug": "replenishment"}` for the operator-level rules.
## Errors
These are the **script path's** error envelope; on the **native path** a failed
tool call surfaces its own error / `isError` result directly — read it and relay
the human-readable message, don't retry blindly or give up silently.
- `config_error` (exit 2) — `ODOO_MCP_URL` / `ODOO_MCP_TOKEN` missing.
- `invalid_arguments` (exit 2) — bad CLI usage or non-JSON arguments.
- `connection_error` — Odoo unreachable, transport error, or the key was
rejected (the MCP server requires a valid key even to list tools).
- A tool that ran but failed returns a normal MCP result with `isError: true`
and a human-readable message in its content — surface that to the user.
## References
- [`references/agent_api_endpoints.md`](references/agent_api_endpoints.md) — the
underlying Odoo operations (now exposed as MCP tools); `tools/list` is
authoritative for the live schema.
- [`references/production_scheduling.md`](references/production_scheduling.md) —
the MRP data model behind the production tools.
- [`references/replenishment_purchasing.md`](references/replenishment_purchasing.md) —
the replenishment → vendor-purchasing loop and the vendor-skill hand-off
contract (dataset in, result out).
## Legacy REST scripts (deprecated)
`scripts/sales.py`, `scripts/ap.py`, `scripts/production.py`, and
`scripts/odoo_client.py` are the previous REST wrappers for the old `agent_api`
module. They still work against the `drivethru_mcp` module's REST back-compat
routes (`/agent_api/v1/*`) but are deprecated — prefer `scripts/odoo_mcp.py`.
They will be removed once all deployments are on the MCP surface.
don't have the plugin yet? install it then click "run inline in claude" again.