Skip to main content
Glama
singleflo

io.github.singleflo/odoo-assistant

by singleflo

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault

No arguments

Instructions

Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.

This server publishes no instructions, or was last inspected before Glama recorded them.

Capabilities

Features and capabilities supported by this server

Protocol revision2025-11-25

CapabilityDetails
tools
{
  "listChanged": false
}
prompts
{
  "listChanged": false
}
resources
{
  "subscribe": false,
  "listChanged": false
}
experimental
{}

Tools

Functions exposed to the LLM to take actions

NameDescription
search_readA

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.

read_recordA

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.

count_recordsA

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].

instance_overviewA

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.

required_fieldsA

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".

create_recordA

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.

write_recordA

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.

run_actionA

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.

cancel_recordC

Cancel a record through action_cancel, following the wizard it returns.

Destructive, so the default ceiling refuses it and says what would not.

notify_userA

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 "x" 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.
create_activityA

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.

download_docsA

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.

generate_pdfA

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.

list_message_targetsA

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.

read_conversationA

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.

send_direct_messageA

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.

send_channel_messageA

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.

explore_moduleB

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.

list_known_modulesA

List the modules this server has learned: name, generation date, records.

Prompts

Interactive templates invoked by user choice

NameDescription

No prompts

Resources

Contextual data attached and managed by the client

NameDescription
odoo-skillHow to operate an Odoo instance: the safety rules, the query and write patterns, and the verification methodology.
odoo-ref-SKILLOdoo methodology reference: SKILL.
odoo-ref-collaborationOdoo methodology reference: collaboration.
odoo-ref-deletionOdoo methodology reference: deletion.
odoo-ref-documentsOdoo methodology reference: documents.
odoo-ref-paymentsOdoo methodology reference: payments.
odoo-ref-recipesOdoo methodology reference: recipes.
odoo-ref-writingOdoo methodology reference: writing.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/singleflo/odoo-assistant-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server