Odoo Assistant
Odoo virtual employee via MCP: query, create, modify records, workflows, PDFs, and activities.
Connector by singleflo · GitHub account verified by the MCP Registry
Install
Works with Claude Code, VS Code, Codex, Cursor, Windsurf, Claude Desktop. Read the repository before you give any tool access to your data.
npx agentmarket install connector/singleflo-odoo-assistant
Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.
Remote endpoint
https://odoomcp.singleflo.com/mcpRemote (HTTP)
Get the weekly diff
The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.
About
Reviews
Write a reviewNo reviews yet. Installed it? Reviews come from verified installers only. How reviews work
Permissions
What this server can do and what it asks for, from its tool list, install config and code. Checked Oct 25, 2026.
- Tools
- 11 read · 10 write · 0 delete · 1 not yet classified
- Secrets
- 1 (API keys, tokens or passwords)
- Install scripts
- None found
- Runs as
- Python package
Tools by what they do 22
Read from the source code; the running server may list different tools.
Read 11 · Look things up without changing anything.
count_recordsdescribe_modelexplore_moduleinstance_overviewlist_known_moduleslist_message_targetsread_conversationread_long_fieldread_recordrequired_fieldssearch_read
Write 10 · Create, change, send or run things.
cancel_recordcreate_activitycreate_recorddownload_docsgroup_recordsnotify_userrun_actionsend_channel_messagesend_direct_messagewrite_record
Not yet classified 1 · We could not tell from the name and description.
generate_pdf
Secrets and settings it asks for
Secrets: values you paste in that let it act as you.
ODOO_API_KEY
Other settings (environment variables or headers).
ODOO_BASE_URLODOO_DBODOO_MCP_ALLOWODOO_MCP_ALLOW_UNLINKODOO_MCP_DENYODOO_USER
Network hosts it talks to 7
agent-plugins.org· found in codechatgpt.com· found in codeclaude.ai· found in codegithub.com· found in codemycompany.odoo.com· found in codeodoomcp.singleflo.com· found in remote endpoint, install command, codesingleflo.com· found in code
Files it touches
/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf- Your home directory (~)
~/.hermes/odoo/instances~/.local/share
Tools (22)
Read from the source code; the running server may differ. Descriptions are the server’s own words; hidden characters are shown as [U+…].
cancel_recordCancel a recordCancel a record through `action_cancel`, following the wizard it returns. `action_cancel` sits in the default ODOO_MCP_DENY list, so this tool is refused until the operator removes that entry from the variable.
count_recordsCount recordsCount 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. One value per call …
create_activitySchedule an activitySchedule 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 thi…
create_recordCreate a recordCreate 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. When the gate refuses, tell the user which entry of …
describe_modelDescribe a modelList a model's fields as the live instance defines them (Odoo `fields_get`). Answers "what can I filter, group or write on this model" in one call — the alternative, reading `ir.model.fields` through `search_read`, takes one call per batch of names and returns rows you then have to join. Required fields are starred. For what a `create` demands — including the default Odoo would apply and how existing records actually use it — use `required_fields`, which reads deeper on exactly that question. Args: model: Odoo model, e.g. "crm.lead".
download_docsDownload a record's documentsSave 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". On the hosted server the result is a download link valid for 15 minutes; `dest_dir` is ignored there. 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 …
explore_moduleExplore a moduleDiscover 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, replacing the generated part of an earlier one (its NOTES section is kept); "list" ranks what is worth exploring and writes nothing. models: Comma-separated models for a module the script does not know, e.g. "superchat.message,superchat.template". Defaults to the…
generate_pdfGenerate a record's PDFRender 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` rather than as a plain read. `action_send_and_print` is allowed by default; add it to ODOO_MCP_DENY to refuse PDFs that could trigger the send wizard. On the hosted server the result is a download link valid for 15 minutes; `dest_dir` is ignored there. Args: model: the Odoo model, e.g. "account.move". record_id: id of the record to print.…
group_recordsGroup recordsGroup records and count (or aggregate) per bucket (Odoo `read_group`). The tool for "how many per state / stage / month": one call returns every existing bucket with its count. Issuing one `count_records` per value is the pattern this tool exists to prevent — N round trips where one answers, and on a live instance half of them come back zero. Args: model: Odoo model, e.g. "crm.lead". domain: Odoo domain, e.g. [["state", "=", "sale"]]. group_by: one or two fields, e.g. ["stage_id"] or ["create_date:month", "stage_id"]. Date granularity: day, week, month, quarter, y…
instance_overviewInstance overviewSummarise 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 …
list_known_modulesList known modulesList the modules this server has learned: name, generation date, records.
list_message_targetsList message targetsWho 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 tha…
notify_userNotify users on a recordWrite a note on a record's chatter and notify the users you name, on the user's behalf. Two calls, so the user approves before anything is posted. The first, with no confirmation_token, posts NOTHING: it answers with a preview — the record, the people notified, who would be emailed and the message — and a token. Show the preview to the user and ask, and say plainly when it lists people who will receive an email. Only once they approve, call again with the same arguments and the token; that call posts. Both subtypes post a message that IS VISIBLE in the record's chatter. The difference is who…
read_conversationRead a conversationRead 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.
read_long_fieldRead a long text fieldRead ONE long text field in windows, so a value larger than the 5000-char result cap stays readable to the end. Every tool result is capped at 5000 characters. When a single value is bigger than that — a `description` holding a pasted transcript, an email `body` — narrowing the domain or the field list cannot help: that one value already IS the whole answer, so the tail is unreachable and the caller ends up asking for the same cut text again. Measured on a live lead: its description came back cut, and no limit or offset could ever reach the rest of it. This reads that field alone and returns…
Show 7 more tools
read_recordRead a recordRead 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. The answer i…
required_fieldsRequired fields for createList 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` …
run_actionRun a workflow actionRun a workflow step on Odoo records — the method behind a button such as Confirm on a quotation or Post on an invoice — and report the state each record is in afterwards. Side effects: this changes records in the user's Odoo, and most steps cannot be undone from here (a posted invoice stays posted). Confirm the records and the step with the user before calling it. A step runs once: calling it again on the same records fails instead of doing nothing, so never retry — re-read the records to see whether it already ran. If the answer carries `res_model`, Odoo opened a wizard that needs more inpu…
search_readSearch recordsSearch 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 …
send_channel_messagePost to a channelPost to an EXISTING Discuss channel, on the user's behalf — everyone in it sees this. Two calls, so the user approves before anything is posted. The first, with no confirmation_token, posts NOTHING: it answers with a preview — the channel, how many members it reaches and the message — and a token. Show the preview to the user and ask. Only once they approve, call again with the same arguments and the token; that call posts. A changed message or channel needs a new preview. The channel is never created here: `list_message_targets` shows the ones that exist, and posting to a room of the wrong …
send_direct_messageSend a direct messageSend a 1-to-1 Discuss message, on the user's behalf, that appears in the recipient's chat systray. Two calls, so the user approves before anything is sent. The first, with no confirmation_token, sends NOTHING: it answers with a preview — the recipient, the route and the message — and a token. Show the preview to the user and ask. Only once they approve, call again with the same arguments and the token; that call sends. A changed message or recipient needs a new preview. This is the tool for "tell X", "message X", "warn X". It opens the private chat with that user — reusing the existing one, …
write_recordUpdate a recordWrite 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 — so the gate matches it as `archive`, which the default ODOO_MCP_DENY list refuses. When the gate refuses, tell the user which entry of ODOO_MCP_DENY (or ODOO_MCP_ALLOW_UNLINK) would allow it and stop; never retry with another method name.
Signals
- Stars
- 1
- Forks
- 3
- Open issues
- 0
- Weekly downloads
- 193
- Last commit
- Oct 22, 2026 (6 days ago)
- Last release
- Oct 22, 2026 (6 days ago)
- Last package publish
- Oct 22, 2026 (6 days ago)
From GitHub (fetched Oct 24, 2026) and the package registry.
Details
- Registry name
io.github.singleflo/odoo-assistant- Version
- 0.3.15
- Auth
- Auth not stated, API key
- Packages
- pypi odoo-assistant
- License
- MIT
- Repository
- github.com/singleflo/odoo-assistant-mcp
- First seen
- Oct 24, 2026
- Last checked
- Oct 24, 2026
More from singleflo
Cite this page
Odoo Assistant MCP server. Agent Mart. https://agentmart.dev/connectors/singleflo-odoo-assistant (last updated 2026-10-24)