io.github.singleflo/odoo-assistant
PYPI · ODOO-ASSISTANT · SCANNED AUG 20
Odoo virtual employee via MCP: query, create, modify records, workflows, PDFs, and activities.
Available components
How this component scores in each security and reliability category. Every signal is checked automatically from public evidence about the published package, including repeated runs of it in an isolated sandbox, and we only credit what we can confirm. How we score →
Supply Chain Security100
- No malware found by supply-chain analysis.Pass
- No known CVEs affecting this package version or its production dependencies.Pass
- Runs hatchling.build at install time, a recognised native-build step with no shell scripting around it. View diagnostics → Pass
- 2 of 30 dependencies flagged as unhealthy. View diagnostics → Partial
Provenance & Transparency35
- Source repository is publicly reachable at the declared URL. View diagnostics → Pass
- Provenance check failed: no build-provenance attestation is published. See how to fix → View diagnostics → Fail
- License check failed: the license (MIT License) isn't a recognized OSI-approved license. See how to fix → Fail
- Actively maintained (last published 1 days ago).Pass
- Publishes a security disclosure policy (SECURITY.md).Pass
Schema Quality & AI Usability81
- 100% of prompts and resources have a non-trivial description (not blank, and not just the item's name).Pass
- AI-judged instruction clarity (good).Pass
- Context-footprint check failed: tool/resource definitions use about 3683 tokens (~136/item across 27 items; 19 tools + 8 resources), over budget; trim descriptions and params. See how to fix → Fail
- Usage-examples check failed: none of the tools include examples. See how to fix → Fail
Stability & Change Management0
- Stability not yet verified: not enough scan history yet (needs a 30-day window).Unverified
Tool Coverage71
- 100% of tools have a non-trivial description (not blank, and not just the tool's name).Pass
- 0% of tool parameters carry a description.Fail
- Structured output schemas are declared (100% of tools); any adoption earns full credit.Pass
Capabilities100
- Implements a current MCP spec version (2026-07-28).Pass
Unverified: 1 category
A category scored 0 because we could not verify it: a data source with nothing on this package, evidence we could not reach, or a check we could not run. We only credit what we can confirm.
Add this component to your MCP client. Where a client-specific snippet is available, pick your client below and copy it straight into your config; otherwise use the connection detail shown.
pypi · odoo-assistant
claude mcp add singleflo-odoo-assistant -- uvx odoo-assistant
codex mcp add singleflo-odoo-assistant -- uvx odoo-assistant
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"singleflo-odoo-assistant": {
"type": "local",
"command": [
"uvx",
"odoo-assistant"
],
"enabled": true
}
}
} openclaw mcp add singleflo-odoo-assistant --command uvx --arg odoo-assistant
mcp_servers:
singleflo-odoo-assistant:
command: "uvx"
args: ["odoo-assistant"] {
"mcpServers": {
"singleflo-odoo-assistant": {
"command": "uvx",
"args": [
"odoo-assistant"
]
}
}
} Every change we have recorded for this component, newest first. Security-relevant changes are always shown. ▲ marks a change for the better, ▼ a change for the worse; unmarked changes are neutral.
- 19 Aug 26 +15
- Malware scan: unverified → pass ▲ security
- 18 Aug 26 50
First indexed and scored.
Diagnostic detail from the automated scan of this channel: what the scanner observed at each step, so you can see exactly where a check passed or failed. It is informational only and never changes the trust score.
Captured 20 Aug 2026 · Analysed pypi/odoo-assistant@0.1.1
Provenance No attestation
The registry publishes no build provenance for this version, so there is nothing to verify.
| Result | No attestation |
|---|---|
| Ecosystem | pypi |
Install scripts 1 script
| Hook | Tier | Command |
|---|---|---|
| build_backend | allowlisted | hatchling.build |
Dependencies 30 packages
| Packages resolved | 30 |
|---|---|
| Stale | 1 |
| No linked repository | 1 |
| Tree resolution | Complete |
The tools this component advertises to a client, with an estimated token cost for each. Expand a tool to see its parameters and schema. The per-tool counts are indicative and are not scored directly; the schema's total context footprint is one signal in Schema Quality & AI Usability.
cancel_record ~52
Cancel a record through `action_cancel`, following the wizard it returns. Destructive, so the default ceiling refuses it and says what would not.
| Name | Type | Req | Description |
|---|---|---|---|
| model | string | yes | – |
| record_id | integer | yes | – |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
count_records ~214
Count the records matching a domain (Odoo `search_count`). A count is only as honest as its domain: * `account.move` / `account.move.line` without a `move_type` filter is refused — it would count invoices, bills, credit notes and journal entries together and match no figure the user has ever seen. * A count answers "how many", never "how much". For an amount, read `amount_total_signed` (company currency) and never `amount_total`. * On a multi-company instance the count differs per company: pass `company_ids` or you are reporting one company as the whole business. Args: model: Odoo model, e.g. "crm.lead". domain: Odoo domain. Omit to count everything the model holds. company_ids: Companies to count in, e.g. [1, 2].
| Name | Type | Req | Description |
|---|---|---|---|
| company_ids | – | – | – |
| domain | – | – | – |
| model | string | yes | – |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
create_activity ~197
Schedule an activity: the only notification that carries a deadline. A chatter note is passive. An activity appears in the assignee's To-Do list and turns overdue when the date passes. Args: model: the Odoo model, e.g. "crm.lead". record_id: id of the record the activity hangs off. summary: the one-line title the assignee will read. user_id: res.users id of the assignee. days: deadline offset from today, in days. activity_type: substring of an activity type name, e.g. "call". Activity types differ per instance; the first available type is used when this is omitted or matches nothing.
| Name | Type | Req | Description |
|---|---|---|---|
| activity_type | – | – | – |
| days | integer | – | – |
| model | string | yes | – |
| record_id | integer | yes | – |
| summary | string | yes | – |
| user_id | integer | yes | – |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
create_record ~162
Create a record, reusing an existing match when `unique_on` is given. `unique_on` is a list of FIELD NAMES taken from `values` (e.g. `["name", "email"]`): they are searched first and the existing id comes back instead of a duplicate. Odoo has no idempotency key, so a create that is retried is simply a second record — this is the only protection there is, and a cold-start run without it produced four identical customers. Multi-company: put `company_id` in `values`. The context decides what is visible, not which company owns the new record.
| Name | Type | Req | Description |
|---|---|---|---|
| model | string | yes | – |
| unique_on | – | – | – |
| values | object | yes | – |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
download_docs ~163
Save every document of a record to disk — chatter files included. Returns {"saved": [paths], "skipped": [[name, why]]}. `skipped` is not noise: a database restored without its filestore keeps the attachment rows and loses the bytes, and an empty result would read exactly like "this record has no attachments". Args: model: the Odoo model, e.g. "account.move". record_id: id of the record whose documents to fetch. dest_dir: directory to write the files into. Defaults to this platform's temporary directory — "/tmp" does not exist on Windows.
| Name | Type | Req | Description |
|---|---|---|---|
| dest_dir | string | – | – |
| model | string | yes | – |
| record_id | integer | yes | – |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
explore_module ~152
Discover a module's structure by interrogating the live instance. Args: module_name: Module to explore, e.g. "helpdesk". Must be a module slug, since it names the reference file on "generate"; ignored on "list". action: "generate" (the default) writes the reference document, "list" ranks what is worth exploring. models: Comma-separated models for a module the script does not know, e.g. "superchat.message,superchat.template". Defaults to the script's own grouping for `module_name`.
| Name | Type | Req | Description |
|---|---|---|---|
| action | string | – | – |
| models | string | – | – |
| module_name | string | yes | – |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
generate_pdf ~158
Render the PDF of a record and return where it was saved. An already rendered PDF is reused. Otherwise the model's own print/send wizard produces it, and that wizard can also SEND the document — which is why this is gated on `action_send_and_print` (L3_STATE_CHANGE) rather than as a plain read. Args: model: the Odoo model, e.g. "account.move". record_id: id of the record to print. dest_dir: directory to write the PDF into. Defaults to this platform's temporary directory — "/tmp" does not exist on Windows.
| Name | Type | Req | Description |
|---|---|---|---|
| dest_dir | string | – | – |
| model | string | yes | – |
| record_id | integer | yes | – |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
instance_overview ~244
Summarise the connected instance: version, companies, volumes per area, in-house modules, anomalies. The profile is built from the instance this server is CONNECTED to, and cached per instance — `census.profile_path()` keys it on the live client, not on an environment variable. That distinction is the whole point: with two instances profiled on one machine, choosing by `ODOO_DB` (which is discovered now, so often unset) once fell through to "the first file on disk" and reported a neighbour's numbers as this instance's, with no error and a perfectly plausible report. First call against a new instance builds the profile, which costs a second or so; every later call is free. Pass `refresh=True` after the instance has changed — the report carries the timestamp it was taken. When drilling into these figures, the two rules that keep them meaningful: filter `account.move` by `move_type`, and sum `amount_total_signed`, never `amount_total`. Args: refresh: rebuild the profile from the instance instead of reusing it.
| Name | Type | Req | Description |
|---|---|---|---|
| refresh | boolean | – | – |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
list_known_modules ~24
List the modules this server has learned: name, generation date, records.
Input schema present but exposes no named parameters.
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
list_message_targets ~242
Who can be messaged and where — ASK THIS BEFORE SENDING ANYTHING. Two lists in one call, because an agent that cannot see the roster invents ids: * `users`: the internal, active users, each with `im_status` — 'online', 'away' (idle 30 minutes) or 'offline'. Presence is worth reading first: a Discuss message is delivered either way, but "offline" tells you nobody is going to answer right now. * `conversations`: the ones the sender already belongs to and has not archived, with `channel_type` — 'chat' is a 1-to-1, 'group' is a private multi-party, 'channel' is a room that may hold the whole company. `members` and `unread` are there so a broadcast is a deliberate choice rather than a surprise. Use `send_direct_message` for a person and `send_channel_message` for a conversation in this list. Neither of them is the tool for annotating an invoice or an order — that is `notify_user`.
Input schema present but exposes no named parameters.
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
notify_user ~445
Write a note on a record's chatter and notify the users you name. Both subtypes post a message that IS VISIBLE in the record's chatter. The difference is who it reaches beyond the people you name. Args: model: the Odoo model, e.g. "sale.order". record_id: id of the record to write on. message: the body. Send PLAIN TEXT: Odoo escapes anything that arrives over RPC, so "<b>x</b>" is displayed as the literal characters `<b>x</b>`, not as bold — there is no way to pass real markup through this call, and newlines survive but are not turned into line breaks. Write the note as prose. user_ids: res.users ids to notify. They are notified each through their OWN Odoo setting, inbox or email, so naming someone is not a promise that no mail leaves. subtype: where the message lands. | subtype | visible in the chatter | emails a customer | |-----------|------------------------|-------------------| | "note" | yes, internal users | never | | "inbox" | NO — notification only | never | | "comment" | yes, everyone | **YES** | "note" posts `mail.mt_note` and is the default: measured on a real order it produced one inbox notification and zero emails. "inbox" goes through `message_notify`, which Odoo documents as the path for "messages that should not be displayed on a document" — the person is notified, the record keeps no trace. "comment" posts `mail.mt_comment` and is refused while an external follower exists, unless force=True. force: post the comment anyway, knowing those people get an email.
| Name | Type | Req | Description |
|---|---|---|---|
| force | boolean | – | – |
| message | string | yes | – |
| model | string | yes | – |
| record_id | integer | yes | – |
| subtype | string | – | – |
| user_ids | array | yes | – |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
read_conversation ~133
Read what was said in a Discuss conversation, newest first. This is how you answer "what did they write to me" or "what is going on in that channel". `list_message_targets` gives you the `channel_id` and says how many messages are unread. Reading does not mark anything as read: the unread counter belongs to the member record and only the user's own client clears it. Args: channel_id: the Discuss channel, from `list_message_targets`. limit: how many recent messages to return.
| Name | Type | Req | Description |
|---|---|---|---|
| channel_id | integer | yes | – |
| limit | integer | – | – |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
read_record ~222
Read one record by id, always with named fields (writing.md pattern 12). Omitting `fields` asks for a short list of state fields — never for all of them: that read is slow at best and fails at worst. `account.move` and `account.move.line` are refused here, because the structural guard wants a `move_type` filter and this tool has nowhere to put one. Use `search_read` with `[["id", "=", <id>], ["move_type", "=", "out_invoice"]]` instead. And never add up `amount_total` across records — it is in the record's own currency. `amount_total_signed` is the company-currency twin to sum. Args: model: Odoo model, e.g. "sale.order". record_id: The record's database id. fields: Field names to read. Omit for the usual state fields.
| Name | Type | Req | Description |
|---|---|---|---|
| fields | – | – | – |
| model | string | yes | – |
| record_id | integer | yes | – |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
required_fields ~211
List the fields Odoo demands before it will accept a `create`, with the default it would apply and how existing records actually use it. Ask this BEFORE `create_record` on a model you have not written to in this session. The answer is read from the live instance — `fields_get` plus `default_get` — never from a table in this file, so a model customised in-house reports its own requirements. The dangerous required field is the one that already carries a default: the create succeeds without you naming it and the record lands wherever the default points, with no error to notice. `crm.lead.type` is the standing example — Odoo defaults it to 'lead', and on an instance that works its pipeline as opportunities that record goes straight to a menu nobody opens. That is why the live distribution is printed beside each default. Args: model: Odoo model, e.g. "crm.lead".
| Name | Type | Req | Description |
|---|---|---|---|
| model | string | yes | – |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
run_action ~125
Run a workflow method and report the state it left behind. The level follows `method`: confirming or posting is a state change, cancelling or unlinking is destructive and refused unless the server's ceiling was raised deliberately. Two behaviours come from the Writer and are worth knowing: a returned dict carrying `res_model` is a wizard to follow rather than a result, and a transition is one-way — calling it twice raises instead of doing nothing.
| Name | Type | Req | Description |
|---|---|---|---|
| method | string | yes | – |
| model | string | yes | – |
| record_ids | array | yes | – |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
search_read ~359
Search and read records in one call (Odoo `search_read`). Two pitfalls this tool cannot fix for you: * `account.move` and `account.move.line` mix customer invoices, vendor bills, credit notes and raw journal entries. A domain without `move_type` is refused — add `["move_type", "=", "out_invoice"]` (or `in_invoice`, `out_refund`, `in_refund`) so the answer matches what the user sees on screen. * NEVER sum `amount_total`: it is expressed in each record's own currency, and eight foreign-currency invoices once inflated a total 11,9×. Ask for `amount_total_signed` instead — any field with a `_signed` twin is stored in company currency, and the twin is the one to add up. Args: model: Odoo model, e.g. "sale.order". domain: Odoo domain, e.g. [["state", "=", "sale"]]. fields: Field names to return. Name them: the default asks for every field, which is slow and can fail to serialise on wide models. limit: Rows to return. Hard-capped at 200. offset: Rows to skip — how to page past a truncated result. company_ids: Companies to read from, e.g. [1, 2]. On a multi-company instance, omitting this reports one company as the whole business.
| Name | Type | Req | Description |
|---|---|---|---|
| company_ids | – | – | – |
| domain | array | yes | – |
| fields | – | – | – |
| limit | integer | – | – |
| model | string | yes | – |
| offset | integer | – | – |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
send_channel_message ~146
Post to an EXISTING Discuss channel — everyone in it sees this. The channel is never created here: `list_message_targets` shows the ones that exist, and posting to a room of the wrong size is not recoverable by deleting the message afterwards. Members who are not employees of this instance — portal users, guests — are named in a refusal rather than written to, the same rule `notify_user` applies to external followers. Nothing is posted in that case. Args: channel_id: from `list_message_targets`. message: the body, plain text or simple HTML.
| Name | Type | Req | Description |
|---|---|---|---|
| channel_id | integer | yes | – |
| message | string | yes | – |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
send_direct_message ~189
Send a 1-to-1 Discuss message that appears in the user's chat systray. This is the tool for "tell X", "message X", "warn X". It opens the private chat with that user — reusing the existing one, `channel_get` matches on the exact pair — and posts there. The bus pushes it in real time and it persists, so a recipient who is offline finds it on their next login. It reaches them whatever their notification setting says, and sends no email at all. That is the difference from `notify_user`, which follows the recipient's preference and lands in the Inbox bell instead. Args: user_id: res.users id of the recipient — from `list_message_targets`. message: the body, plain text or simple HTML.
| Name | Type | Req | Description |
|---|---|---|---|
| message | string | yes | – |
| user_id | integer | yes | – |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
write_record ~108
Write field values to one record and report what actually changed. Writing the value a record already holds succeeds and changes nothing; only the before/after comparison tells that apart from a real update, so that comparison is the answer. Setting `active` to False archives the record — the same visible outcome as deleting it — and is classified destructive rather than as a plain write.
| Name | Type | Req | Description |
|---|---|---|---|
| model | string | yes | – |
| record_id | integer | yes | – |
| values | object | yes | – |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.